From 0183e8aa7d058b6a3137f0d67ee78bbffdad844f Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 12:06:53 -0800 Subject: [PATCH 01/27] refactor(core): move `install-dependencies` to `core-utils` --- .../core/spec/fast/init-scripts/init-npm.spec.ts | 14 +++++++------- .../spec/slow/install-dependencies.slow.spec.ts | 7 ++++--- packages/api/core/src/api/import.ts | 8 +++----- packages/api/core/src/api/init-scripts/init-npm.ts | 13 ++++++------- packages/api/core/src/api/init.ts | 13 +++++++------ .../core-utils/spec}/install-dependencies.spec.ts | 11 ++++------- packages/utils/core-utils/src/index.ts | 1 + .../core-utils/src}/install-dependencies.ts | 3 ++- 8 files changed, 34 insertions(+), 36 deletions(-) rename packages/{api/core/spec/fast/util => utils/core-utils/spec}/install-dependencies.spec.ts (90%) rename packages/{api/core/src/util => utils/core-utils/src}/install-dependencies.ts (94%) 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 09aa5772f4..d7f678e1ec 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,15 +1,15 @@ -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'; + 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'; -vi.mock('../../../src/util/install-dependencies', async (importOriginal) => ({ +vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => ({ ...(await importOriginal()), installDependencies: vi.fn(), })); 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 0c518c53c9..b2a39c2f47 100644 --- a/packages/api/core/spec/slow/install-dependencies.slow.spec.ts +++ b/packages/api/core/spec/slow/install-dependencies.slow.spec.ts @@ -2,11 +2,12 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { PACKAGE_MANAGERS } from '@electron-forge/core-utils'; +import { + installDependencies, + PACKAGE_MANAGERS, +} from '@electron-forge/core-utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { installDependencies } from '../../src/util/install-dependencies'; - describe.runIf(!(process.platform === 'linux' && process.env.CI))( 'install-dependencies', () => { diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index 8367131bb3..8217df5c98 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -1,6 +1,9 @@ import path from 'node:path'; import { + DepType, + DepVersionRestriction, + installDependencies, PMDetails, resolvePackageManager, updateElectronDependency, @@ -17,11 +20,6 @@ import fs from 'fs-extra'; import { Listr } from 'listr2'; import { merge } from 'lodash'; -import { - DepType, - DepVersionRestriction, - installDependencies, -} from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; import { initGit } from './init-scripts/init-git'; 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 442304d97b..f540493222 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -1,16 +1,15 @@ import path from 'node:path'; -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, DepVersionRestriction, installDependencies, -} from '../../util/install-dependencies'; + 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'; const d = debug('electron-forge:init:npm'); const corePackage = fs.readJsonSync( diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index c2c726565b..0905b007ee 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -1,6 +1,12 @@ import path from 'node:path'; -import { PMDetails, resolvePackageManager } from '@electron-forge/core-utils'; +import { + DepType, + DepVersionRestriction, + installDependencies, + PMDetails, + resolvePackageManager, +} from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; import { spawn } from '@malept/cross-spawn-promise'; import chalk from 'chalk'; @@ -8,11 +14,6 @@ import debug from 'debug'; import { Listr } from 'listr2'; import semver from 'semver'; -import { - DepType, - DepVersionRestriction, - installDependencies, -} from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; import { findTemplate } from './init-scripts/find-template'; diff --git a/packages/api/core/spec/fast/util/install-dependencies.spec.ts b/packages/utils/core-utils/spec/install-dependencies.spec.ts similarity index 90% rename from packages/api/core/spec/fast/util/install-dependencies.spec.ts rename to packages/utils/core-utils/spec/install-dependencies.spec.ts index 27cfe8f1e0..a4be23269f 100644 --- a/packages/api/core/spec/fast/util/install-dependencies.spec.ts +++ b/packages/utils/core-utils/spec/install-dependencies.spec.ts @@ -1,17 +1,14 @@ -import { - PACKAGE_MANAGERS, - spawnPackageManager, -} from '@electron-forge/core-utils'; import { describe, expect, it, vi } from 'vitest'; import { DepType, DepVersionRestriction, installDependencies, -} from '../../../src/util/install-dependencies'; +} from '../src/install-dependencies'; +import { PACKAGE_MANAGERS, spawnPackageManager } from '../src/package-manager'; -vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { - const mod = await importOriginal(); +vi.mock('../src/package-manager', async (importOriginal) => { + const mod = await importOriginal(); return { ...mod, spawnPackageManager: vi.fn(), diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index df2fe5fcf1..2c1195fa85 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -2,3 +2,4 @@ export * from './rebuild'; export * from './electron-version'; export * from './package-manager'; export * from './author-name'; +export * from './install-dependencies'; diff --git a/packages/api/core/src/util/install-dependencies.ts b/packages/utils/core-utils/src/install-dependencies.ts similarity index 94% rename from packages/api/core/src/util/install-dependencies.ts rename to packages/utils/core-utils/src/install-dependencies.ts index 81bb3312a9..67215b80e8 100644 --- a/packages/api/core/src/util/install-dependencies.ts +++ b/packages/utils/core-utils/src/install-dependencies.ts @@ -1,7 +1,8 @@ -import { PMDetails, spawnPackageManager } from '@electron-forge/core-utils'; import { ExitError } from '@malept/cross-spawn-promise'; import debug from 'debug'; +import { PMDetails, spawnPackageManager } from './package-manager'; + const d = debug('electron-forge:dependency-installer'); export enum DepType { From 855422f5ca88d70475d526d26b593e005065b908 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 14:28:27 -0800 Subject: [PATCH 02/27] checkpoint --- packages/api/cli/src/electron-forge-import.ts | 32 -- packages/api/cli/src/electron-forge.ts | 1 - packages/api/core/package.json | 5 - packages/api/core/spec/fast/publish.spec.ts | 6 +- packages/api/core/src/api/import.ts | 399 ------------------ packages/api/core/src/api/index.ts | 21 - packages/api/core/src/api/make.ts | 3 +- packages/api/core/src/api/package.ts | 2 +- packages/api/core/src/api/publish.ts | 2 +- .../api/core/src/util/plugin-interface.ts | 3 +- .../external/create-electron-app/package.json | 19 +- .../spec/fast/init-scripts/init-git.spec.ts | 2 +- .../spec/fast/init-scripts/init-npm.spec.ts | 2 +- .../spec/slow/import.slow.verdaccio.spec.ts | 5 +- .../create-electron-app/src/core.ts} | 20 +- .../create-electron-app/src/import-core.ts | 373 ++++++++++++++++ .../external/create-electron-app/src/index.ts | 39 +- .../src}/init-scripts/find-template.ts | 3 +- .../src}/init-scripts/init-directory.ts | 0 .../src}/init-scripts/init-git.ts | 0 .../src}/init-scripts/init-link.ts | 16 +- .../src}/init-scripts/init-npm.ts | 0 .../create-electron-app/src/init.ts} | 29 +- .../src/util/resolve-working-dir.ts | 24 ++ .../core-utils/helper/dynamic-import.d.ts | 3 + .../utils/core-utils/helper/dynamic-import.js | 23 + packages/utils/core-utils/package.json | 1 + .../fixture/require-search/throw-error.js | 0 .../core-utils/spec/import-search.spec.ts | 22 + .../core-utils/src}/import-search.ts | 12 +- packages/utils/core-utils/src/index.ts | 1 + 31 files changed, 563 insertions(+), 505 deletions(-) delete mode 100644 packages/api/cli/src/electron-forge-import.ts delete mode 100644 packages/api/core/src/api/import.ts rename packages/{api/core => external/create-electron-app}/spec/fast/init-scripts/init-git.spec.ts (97%) rename packages/{api/core => external/create-electron-app}/spec/fast/init-scripts/init-npm.spec.ts (95%) rename packages/{api/core => external/create-electron-app}/spec/slow/import.slow.verdaccio.spec.ts (91%) rename packages/{api/core/src/api/init.ts => external/create-electron-app/src/core.ts} (96%) create mode 100644 packages/external/create-electron-app/src/import-core.ts rename packages/{api/core/src/api => external/create-electron-app/src}/init-scripts/find-template.ts (95%) rename packages/{api/core/src/api => external/create-electron-app/src}/init-scripts/init-directory.ts (100%) rename packages/{api/core/src/api => external/create-electron-app/src}/init-scripts/init-git.ts (100%) rename packages/{api/core/src/api => external/create-electron-app/src}/init-scripts/init-link.ts (94%) rename packages/{api/core/src/api => external/create-electron-app/src}/init-scripts/init-npm.ts (100%) rename packages/{api/cli/src/electron-forge-init.ts => external/create-electron-app/src/init.ts} (88%) create mode 100644 packages/external/create-electron-app/src/util/resolve-working-dir.ts create mode 100644 packages/utils/core-utils/helper/dynamic-import.d.ts create mode 100644 packages/utils/core-utils/helper/dynamic-import.js rename packages/{api/core => utils/core-utils}/spec/fixture/require-search/throw-error.js (100%) create mode 100644 packages/utils/core-utils/spec/import-search.spec.ts rename packages/{api/core/src/util => utils/core-utils/src}/import-search.ts (89%) diff --git a/packages/api/cli/src/electron-forge-import.ts b/packages/api/cli/src/electron-forge-import.ts deleted file mode 100644 index fbde84eedf..0000000000 --- a/packages/api/cli/src/electron-forge-import.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { api } from '@electron-forge/core'; -import { program } from 'commander'; - -import './util/terminate'; -import packageJSON from '../package.json'; - -import { resolveWorkingDir } from './util/resolve-working-dir'; - -program - .version(packageJSON.version, '-V, --version', 'Output the current version.') - .helpOption('-h, --help', 'Output usage information.') - .argument( - '[dir]', - 'Directory of the project to import. (default: current directory)', - ) - .option( - '--skip-git', - 'Skip initializing a git repository in the imported project.', - false, - ) - .action(async (dir: string) => { - const workingDir = resolveWorkingDir(dir, false); - - const options = program.opts(); - - await api.import({ - dir: workingDir, - interactive: true, - skipGit: !!options.skipGit, - }); - }) - .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 89a0f1d9e1..617ee3553a 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -26,7 +26,6 @@ program .version(packageJSON.version, '-V, --version', 'Output the current version.') .helpOption('-h, --help', 'Output usage information.') .command('init', 'Initialize a new Electron application.') - .command('import', 'Import an existing Electron project to Forge.') .command( 'start', 'Start the current Electron application in development mode.', diff --git a/packages/api/core/package.json b/packages/api/core/package.json index 71fe1bbcef..f139473469 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -33,11 +33,6 @@ "@electron-forge/plugin-base": "workspace:*", "@electron-forge/publisher-base": "workspace:*", "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*", - "@electron-forge/template-vite": "workspace:*", - "@electron-forge/template-vite-typescript": "workspace:*", - "@electron-forge/template-webpack": "workspace:*", - "@electron-forge/template-webpack-typescript": "workspace:*", "@electron-forge/tracer": "workspace:*", "@electron/get": "^3.0.0", "@electron/packager": "^18.3.5", diff --git a/packages/api/core/spec/fast/publish.spec.ts b/packages/api/core/spec/fast/publish.spec.ts index 860cccd13d..a94322e078 100644 --- a/packages/api/core/spec/fast/publish.spec.ts +++ b/packages/api/core/spec/fast/publish.spec.ts @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { importSearch } from '@electron-forge/core-utils'; import { ForgeMakeResult, ResolvedForgeConfig, @@ -11,7 +12,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { listrMake } from '../../src/api/make'; import publish from '../../src/api/publish'; import findConfig from '../../src/util/forge-config'; -import importSearch from '../../src/util/import-search'; vi.mock(import('../../src/api/make'), async (importOriginal) => { const mod = await importOriginal(); @@ -37,11 +37,11 @@ vi.mock(import('../../src/util/resolve-dir'), async (importOriginal) => { }; }); -vi.mock(import('../../src/util/import-search'), async (importOriginal) => { +vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { const mod = await importOriginal(); return { ...mod, - default: vi.fn(), + importSearch: vi.fn(), }; }); diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts deleted file mode 100644 index 8217df5c98..0000000000 --- a/packages/api/core/src/api/import.ts +++ /dev/null @@ -1,399 +0,0 @@ -import path from 'node:path'; - -import { - DepType, - DepVersionRestriction, - installDependencies, - PMDetails, - resolvePackageManager, - updateElectronDependency, -} from '@electron-forge/core-utils'; -import { - ForgeListrOptions, - ForgeListrTaskFn, -} from '@electron-forge/shared-types'; -import baseTemplate from '@electron-forge/template-base'; -import { autoTrace } from '@electron-forge/tracer'; -import chalk from 'chalk'; -import debug from 'debug'; -import fs from 'fs-extra'; -import { Listr } from 'listr2'; -import { merge } from 'lodash'; - -import { readRawPackageJson } from '../util/read-package-json'; - -import { initGit } from './init-scripts/init-git'; -import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm'; - -const d = debug('electron-forge:import'); - -export interface ImportOptions { - /** - * The path to the app to be imported - */ - dir?: string; - /** - * Whether to use sensible defaults or prompt the user visually - */ - interactive?: boolean; - /** - * An async function that returns true or false in order to confirm the start - * of importing - */ - confirmImport?: () => Promise; - /** - * An async function that returns whether the import should continue if it - * looks like a forge project already - */ - shouldContinueOnExisting?: () => Promise; - /** - * An async function that returns whether the given dependency should be removed - */ - shouldRemoveDependency?: ( - dependency: string, - explanation: string, - ) => Promise; - /** - * An async function that returns whether the given script should be overridden with a forge one - */ - shouldUpdateScript?: ( - scriptName: string, - newValue: string, - ) => Promise; - /** - * The path to the directory containing generated distributables - */ - outDir?: string; - /** - * By default, Forge initializes a git repository in the project directory. Set this option to `true` to skip this step. - */ - skipGit?: boolean; -} - -export default autoTrace( - { name: 'import()', category: '@electron-forge/core' }, - async ( - childTrace, - { - dir = process.cwd(), - interactive = false, - confirmImport, - shouldContinueOnExisting, - shouldRemoveDependency, - shouldUpdateScript, - outDir, - skipGit = false, - }: ImportOptions, - ): Promise => { - const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { - concurrent: false, - rendererOptions: { - collapseSubtasks: false, - collapseErrors: false, - }, - silentRendererCondition: !interactive, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), - }; - - const runner = new Listr( - [ - { - title: 'Locating importable project', - task: childTrace( - { name: 'locate-project', category: '@electron-forge/core' }, - async () => { - d(`Attempting to import project in: ${dir}`); - if ( - !(await fs.pathExists(dir)) || - !(await fs.pathExists(path.resolve(dir, 'package.json'))) - ) { - throw new Error( - `We couldn't find a project with a package.json file in: ${dir}`, - ); - } - - if (typeof confirmImport === 'function') { - if (!(await confirmImport())) { - // TODO: figure out if we can just return early here - // eslint-disable-next-line no-process-exit - process.exit(0); - } - } - - if (!skipGit) { - await initGit(dir); - } - }, - ), - }, - { - title: 'Processing configuration and dependencies', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, - }, - task: childTrace>( - { name: 'string', category: 'foo' }, - async (_, ctx, task) => { - const calculatedOutDir = outDir || 'out'; - - 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.version) { - task.output = chalk.yellow( - `Please set the ${chalk.green('"version"')} in your application's package.json`, - ); - } - if (packageJSON.config && packageJSON.config.forge) { - if (packageJSON.config.forge.makers) { - task.output = chalk.green( - 'Existing Electron Forge configuration detected', - ); - if (typeof shouldContinueOnExisting === 'function') { - if (!(await shouldContinueOnExisting())) { - // TODO: figure out if we can just return early here - // eslint-disable-next-line no-process-exit - process.exit(0); - } - } - } else if (!(typeof packageJSON.config.forge === 'object')) { - task.output = chalk.yellow( - "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", - ); - } - } - - 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: Record = { - '@electron/get': - 'already uses this module as a transitive dependency', - '@electron/osx-sign': - 'already uses this module as a transitive dependency', - '@electron/packager': - 'already uses this module as a transitive dependency', - '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', - 'electron-installer-redhat': - 'already uses this module as a transitive dependency', - 'electron-winstaller': - 'already uses this module as a transitive dependency', - }; - - for (const key of keys) { - if (buildToolPackages[key]) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const explanation = buildToolPackages[key]!; - let remove = true; - if (typeof shouldRemoveDependency === 'function') { - remove = await shouldRemoveDependency(key, explanation); - } - - if (remove) { - delete packageJSON.dependencies[key]; - delete packageJSON.devDependencies[key]; - } - } - } - - packageJSON.scripts = packageJSON.scripts || {}; - d('reading current scripts object:', packageJSON.scripts); - - const updatePackageScript = async ( - scriptName: string, - newValue: string, - ) => { - if (packageJSON.scripts[scriptName] !== newValue) { - let update = true; - if (typeof shouldUpdateScript === 'function') { - update = await shouldUpdateScript(scriptName, newValue); - } - if (update) { - packageJSON.scripts[scriptName] = newValue; - } - } - }; - - await updatePackageScript('start', 'electron-forge start'); - await updatePackageScript('package', 'electron-forge package'); - await updatePackageScript('make', 'electron-forge make'); - - d('forgified scripts object:', packageJSON.scripts); - - const writeChanges = async () => { - await fs.writeJson( - path.resolve(dir, 'package.json'), - packageJSON, - { spaces: 2 }, - ); - }; - - return task.newListr<{ pm: PMDetails }>( - [ - { - title: `Resolving package manager`, - task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(); - task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; - }, - }, - { - title: 'Configuring Yarn (if applicable)', - task: async ({ pm }) => { - // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config - // lets ensure that nodeLinker is set to node-modules - if (pm.executable === 'yarn') { - const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); - if (!(await fs.pathExists(yarnrcPath))) { - d( - 'creating .yarnrc.yml with nodeLinker: node-modules', - ); - await fs.writeFile( - yarnrcPath, - 'nodeLinker: node-modules\n', - ); - } - } - }, - }, - { - title: 'Installing dependencies', - task: async ({ pm }, task) => { - await writeChanges(); - - d('deleting old dependencies forcefully'); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron'), - ); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron.cmd'), - ); - - d('installing dependencies'); - task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; - await installDependencies(pm, dir, importDeps); - - d('installing devDependencies'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; - 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 installDependencies( - pm, - dir, - importExactDevDeps, - DepType.DEV, - DepVersionRestriction.EXACT, - ); - }, - }, - { - title: 'Copying base template Forge configuration', - task: async () => { - const pathToTemplateConfig = path.resolve( - baseTemplate.templateDir, - 'forge.config.js', - ); - - // if there's an existing config.forge object in package.json - if ( - packageJSON?.config?.forge && - typeof packageJSON.config.forge === 'object' - ) { - d( - 'detected existing Forge config in package.json, merging with base template Forge config', - ); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const templateConfig = require( - path.resolve( - baseTemplate.templateDir, - 'forge.config.js', - ), - ); - packageJSON = await readRawPackageJson(dir); - merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object - await writeChanges(); - // otherwise, write to forge.config.js - } else { - d('writing new forge.config.js'); - await fs.copyFile( - pathToTemplateConfig, - path.resolve(dir, 'forge.config.js'), - ); - } - }, - }, - { - title: 'Fixing .gitignore', - task: async () => { - if ( - await fs.pathExists(path.resolve(dir, '.gitignore')) - ) { - const gitignore = await fs.readFile( - path.resolve(dir, '.gitignore'), - ); - if (!gitignore.includes(calculatedOutDir)) { - await fs.writeFile( - path.resolve(dir, '.gitignore'), - `${gitignore}\n${calculatedOutDir}/`, - ); - } - } - }, - }, - ], - listrOptions, - ); - }, - ), - }, - { - title: 'Finalizing import', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, - }, - task: childTrace>( - { name: 'finalize-import', category: '@electron-forge/core' }, - (_, __, task) => { - task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. - - Thanks for using ${chalk.green('Electron Forge')}!`; - }, - ), - }, - ], - listrOptions, - ); - - await runner.run(); - }, -); diff --git a/packages/api/core/src/api/index.ts b/packages/api/core/src/api/index.ts index 5a358595ac..f7b860480e 100644 --- a/packages/api/core/src/api/index.ts +++ b/packages/api/core/src/api/index.ts @@ -3,31 +3,12 @@ import { ElectronProcess, ForgeMakeResult } from '@electron-forge/shared-types'; // eslint-disable-next-line n/no-missing-import import ForgeUtils from '../util'; -import _import, { ImportOptions } from './import'; -import init, { InitOptions } from './init'; import make, { MakeOptions } from './make'; import _package, { PackageOptions } from './package'; import publish, { PublishOptions } from './publish'; import start, { StartOptions } from './start'; export class ForgeAPI { - /** - * Attempt to import a given module directory to the Electron Forge standard. - * - * * Sets up `git` and the correct NPM dependencies - * * Adds a template forge config to `package.json` - */ - import(opts: ImportOptions): Promise { - return _import(opts); - } - - /** - * Initialize a new Electron Forge template project in the given directory. - */ - init(opts: InitOptions): Promise { - return init(opts); - } - /** * Make distributables for an Electron application */ @@ -66,8 +47,6 @@ export { ForgeMakeResult, ElectronProcess, ForgeUtils, - ImportOptions, - InitOptions, MakeOptions, PackageOptions, PublishOptions, diff --git a/packages/api/core/src/api/make.ts b/packages/api/core/src/api/make.ts index f9f17bb623..4136d3e108 100644 --- a/packages/api/core/src/api/make.ts +++ b/packages/api/core/src/api/make.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { getHostArch } from '@electron/get'; -import { getElectronVersion } from '@electron-forge/core-utils'; +import { getElectronVersion, importSearch } from '@electron-forge/core-utils'; import { MakerBase } from '@electron-forge/maker-base'; import { ForgeArch, @@ -22,7 +22,6 @@ import logSymbols from 'log-symbols'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runMutatingHook } from '../util/hook'; -import importSearch from '../util/import-search'; import getCurrentOutDir from '../util/out-dir'; import parseArchs from '../util/parse-archs'; import { readMutatedPackageJson } from '../util/read-package-json'; diff --git a/packages/api/core/src/api/package.ts b/packages/api/core/src/api/package.ts index 0dd30927c1..560ed3d26f 100644 --- a/packages/api/core/src/api/package.ts +++ b/packages/api/core/src/api/package.ts @@ -11,6 +11,7 @@ import { } from '@electron/packager'; import { getElectronVersion, + importSearch, listrCompatibleRebuildHook, } from '@electron-forge/core-utils'; import { @@ -30,7 +31,6 @@ import { Listr, PRESET_TIMER } from 'listr2'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runHook } from '../util/hook'; -import importSearch from '../util/import-search'; import { warn } from '../util/messages'; import getCurrentOutDir from '../util/out-dir'; import { readMutatedPackageJson } from '../util/read-package-json'; diff --git a/packages/api/core/src/api/publish.ts b/packages/api/core/src/api/publish.ts index 30e621284d..20df46a19c 100644 --- a/packages/api/core/src/api/publish.ts +++ b/packages/api/core/src/api/publish.ts @@ -1,5 +1,6 @@ import path from 'node:path'; +import { importSearch } from '@electron-forge/core-utils'; import { PublisherBase } from '@electron-forge/publisher-base'; import { ForgeConfigPublisher, @@ -19,7 +20,6 @@ import fs from 'fs-extra'; import { Listr } from 'listr2'; import getForgeConfig from '../util/forge-config'; -import importSearch from '../util/import-search'; import getCurrentOutDir from '../util/out-dir'; import PublishState from '../util/publish-state'; import resolveDir from '../util/resolve-dir'; diff --git a/packages/api/core/src/util/plugin-interface.ts b/packages/api/core/src/util/plugin-interface.ts index d56cbf647b..e9f0063e53 100644 --- a/packages/api/core/src/util/plugin-interface.ts +++ b/packages/api/core/src/util/plugin-interface.ts @@ -1,3 +1,4 @@ +import { importSearch } from '@electron-forge/core-utils'; import { PluginBase } from '@electron-forge/plugin-base'; import { ForgeListrTaskDefinition, @@ -17,8 +18,6 @@ import debug from 'debug'; // eslint-disable-next-line n/no-missing-import import { StartOptions } from '../api'; -import importSearch from './import-search'; - const d = debug('electron-forge:plugins'); function isForgePlugin(plugin: IForgePlugin | unknown): plugin is IForgePlugin { diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 0f39b25eb7..e1388d78cf 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -8,7 +8,24 @@ "author": "Samuel Attard", "license": "MIT", "dependencies": { - "@electron-forge/cli": "workspace:*" + "@electron-forge/core-utils": "workspace:*", + "@electron-forge/shared-types": "workspace:*", + "@electron-forge/template-base": "workspace:*", + "@electron-forge/template-vite": "workspace:*", + "@electron-forge/template-vite-typescript": "workspace:*", + "@electron-forge/template-webpack": "workspace:*", + "@electron-forge/template-webpack-typescript": "workspace:*", + "@inquirer/prompts": "^6.0.1", + "@listr2/prompt-adapter-inquirer": "^2.0.22", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "commander": "^11.1.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "listr2": "^7.0.2", + "lodash": "^4.17.20", + "log-symbols": "^4.0.0", + "semver": "^7.2.1" }, "bin": "dist/index.js", "files": [ diff --git a/packages/api/core/spec/fast/init-scripts/init-git.spec.ts b/packages/external/create-electron-app/spec/fast/init-scripts/init-git.spec.ts similarity index 97% rename from packages/api/core/spec/fast/init-scripts/init-git.spec.ts rename to packages/external/create-electron-app/spec/fast/init-scripts/init-git.spec.ts index aefae37aad..02e4f8190b 100644 --- a/packages/api/core/spec/fast/init-scripts/init-git.spec.ts +++ b/packages/external/create-electron-app/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/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/external/create-electron-app/spec/fast/init-scripts/init-npm.spec.ts similarity index 95% rename from packages/api/core/spec/fast/init-scripts/init-npm.spec.ts rename to packages/external/create-electron-app/spec/fast/init-scripts/init-npm.spec.ts index d7f678e1ec..7bc6c3b53e 100644 --- a/packages/api/core/spec/fast/init-scripts/init-npm.spec.ts +++ b/packages/external/create-electron-app/spec/fast/init-scripts/init-npm.spec.ts @@ -7,7 +7,7 @@ import { 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 { deps, devDeps, initNPM } from '../../../src/init-scripts/init-npm'; vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => ({ ...(await importOriginal()), diff --git a/packages/api/core/spec/slow/import.slow.verdaccio.spec.ts b/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts similarity index 91% rename from packages/api/core/spec/slow/import.slow.verdaccio.spec.ts rename to packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts index 447dff7817..2480a8044b 100644 --- a/packages/api/core/spec/slow/import.slow.verdaccio.spec.ts +++ b/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts @@ -2,13 +2,14 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { api } from '@electron-forge/core'; import { ensureTestDirIsNonexistent, updatePackageJSON, } from '@electron-forge/test-utils'; import { beforeEach, describe, expect, it } from 'vitest'; -import { api } from '../../src/api/index'; +import { forgeImport } from '../../src/import-core'; describe('import', () => { let dir: string; @@ -37,7 +38,7 @@ describe('import', () => { // FIXME: the install here will use the production version of Electron Forge // instead of the contents of this monorepo. - await api.import({ dir }); + await forgeImport({ dir }); expect(fs.existsSync(path.join(dir, 'forge.config.js'))).toEqual(true); diff --git a/packages/api/core/src/api/init.ts b/packages/external/create-electron-app/src/core.ts similarity index 96% rename from packages/api/core/src/api/init.ts rename to packages/external/create-electron-app/src/core.ts index 0905b007ee..63ee896eea 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/external/create-electron-app/src/core.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { @@ -14,8 +15,6 @@ import debug from 'debug'; import { Listr } from 'listr2'; import semver from 'semver'; -import { readRawPackageJson } from '../util/read-package-json'; - import { findTemplate } from './init-scripts/find-template'; import { initDirectory } from './init-scripts/init-directory'; import { initGit } from './init-scripts/init-git'; @@ -72,9 +71,14 @@ async function validateTemplate( ); } - const forgeVersion = ( - await readRawPackageJson(path.join(__dirname, '..', '..')) - ).version; + const dir = path.join(__dirname, '..', '..'); + const raw = await fs.promises.readFile( + path.join(dir, 'package.json'), + 'utf-8', + ); + const packageJSON = JSON.parse(raw); + + const forgeVersion = packageJSON.version; if (!semver.satisfies(forgeVersion, templateModule.requiredForgeVersion)) { throw new Error( `Template (${template}) is not compatible with this version of Electron Forge (${forgeVersion}), it requires ${templateModule.requiredForgeVersion}`, @@ -82,7 +86,7 @@ async function validateTemplate( } } -export default async ({ +export async function init({ dir = process.cwd(), interactive = false, copyCIFiles = false, @@ -91,7 +95,7 @@ export default async ({ skipGit = false, electronVersion = 'latest', packageManager, -}: InitOptions): Promise => { +}: InitOptions): Promise { d(`Initializing in: ${dir}`); const runner = new Listr<{ @@ -271,4 +275,4 @@ export default async ({ ); await runner.run(); -}; +} diff --git a/packages/external/create-electron-app/src/import-core.ts b/packages/external/create-electron-app/src/import-core.ts new file mode 100644 index 0000000000..c5f3f96646 --- /dev/null +++ b/packages/external/create-electron-app/src/import-core.ts @@ -0,0 +1,373 @@ +import path from 'node:path'; + +import { + DepType, + DepVersionRestriction, + installDependencies, + PMDetails, + resolvePackageManager, + updateElectronDependency, +} from '@electron-forge/core-utils'; +import { ForgeListrOptions } from '@electron-forge/shared-types'; +import baseTemplate from '@electron-forge/template-base'; +import chalk from 'chalk'; +import debug from 'debug'; +import fs from 'fs-extra'; +import { Listr } from 'listr2'; +import { merge } from 'lodash'; + +import { initGit } from './init-scripts/init-git'; +import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm'; + +const d = debug('electron-forge:import'); + +export interface ImportOptions { + /** + * The path to the app to be imported + */ + dir?: string; + /** + * Whether to use sensible defaults or prompt the user visually + */ + interactive?: boolean; + /** + * An async function that returns true or false in order to confirm the start + * of importing + */ + confirmImport?: () => Promise; + /** + * An async function that returns whether the import should continue if it + * looks like a forge project already + */ + shouldContinueOnExisting?: () => Promise; + /** + * An async function that returns whether the given dependency should be removed + */ + shouldRemoveDependency?: ( + dependency: string, + explanation: string, + ) => Promise; + /** + * An async function that returns whether the given script should be overridden with a forge one + */ + shouldUpdateScript?: ( + scriptName: string, + newValue: string, + ) => Promise; + /** + * The path to the directory containing generated distributables + */ + outDir?: string; + /** + * By default, Forge initializes a git repository in the project directory. Set this option to `true` to skip this step. + */ + skipGit?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const readRawPackageJson = async (dir: string): Promise => + fs.readJson(path.resolve(dir, 'package.json')); + +export async function forgeImport({ + dir = process.cwd(), + interactive = false, + confirmImport, + shouldContinueOnExisting, + shouldRemoveDependency, + shouldUpdateScript, + outDir, + skipGit = false, +}: ImportOptions): Promise { + const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { + concurrent: false, + rendererOptions: { + collapseSubtasks: false, + collapseErrors: false, + }, + silentRendererCondition: !interactive, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }; + + const runner = new Listr( + [ + { + title: 'Locating importable project', + task: async () => { + d(`Attempting to import project in: ${dir}`); + if ( + !(await fs.pathExists(dir)) || + !(await fs.pathExists(path.resolve(dir, 'package.json'))) + ) { + throw new Error( + `We couldn't find a project with a package.json file in: ${dir}`, + ); + } + + if (typeof confirmImport === 'function') { + if (!(await confirmImport())) { + // TODO: figure out if we can just return early here + // eslint-disable-next-line no-process-exit + process.exit(0); + } + } + + if (!skipGit) { + await initGit(dir); + } + }, + }, + { + title: 'Processing configuration and dependencies', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: async (_ctx: any, task: any) => { + const calculatedOutDir = outDir || 'out'; + + 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.version) { + task.output = chalk.yellow( + `Please set the ${chalk.green('"version"')} in your application's package.json`, + ); + } + if (packageJSON.config && packageJSON.config.forge) { + if (packageJSON.config.forge.makers) { + task.output = chalk.green( + 'Existing Electron Forge configuration detected', + ); + if (typeof shouldContinueOnExisting === 'function') { + if (!(await shouldContinueOnExisting())) { + // TODO: figure out if we can just return early here + // eslint-disable-next-line no-process-exit + process.exit(0); + } + } + } else if (!(typeof packageJSON.config.forge === 'object')) { + task.output = chalk.yellow( + "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", + ); + } + } + + 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: Record = { + '@electron/get': + 'already uses this module as a transitive dependency', + '@electron/osx-sign': + 'already uses this module as a transitive dependency', + '@electron/packager': + 'already uses this module as a transitive dependency', + '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', + 'electron-installer-redhat': + 'already uses this module as a transitive dependency', + 'electron-winstaller': + 'already uses this module as a transitive dependency', + }; + + for (const key of keys) { + if (buildToolPackages[key]) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const explanation = buildToolPackages[key]!; + let remove = true; + if (typeof shouldRemoveDependency === 'function') { + remove = await shouldRemoveDependency(key, explanation); + } + + if (remove) { + delete packageJSON.dependencies[key]; + delete packageJSON.devDependencies[key]; + } + } + } + + packageJSON.scripts = packageJSON.scripts || {}; + d('reading current scripts object:', packageJSON.scripts); + + const updatePackageScript = async ( + scriptName: string, + newValue: string, + ) => { + if (packageJSON.scripts[scriptName] !== newValue) { + let update = true; + if (typeof shouldUpdateScript === 'function') { + update = await shouldUpdateScript(scriptName, newValue); + } + if (update) { + packageJSON.scripts[scriptName] = newValue; + } + } + }; + + await updatePackageScript('start', 'electron-forge start'); + await updatePackageScript('package', 'electron-forge package'); + await updatePackageScript('make', 'electron-forge make'); + + d('forgified scripts object:', packageJSON.scripts); + + const writeChanges = async () => { + await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { + spaces: 2, + }); + }; + + return task.newListr( + [ + { + title: `Resolving package manager`, + task: async (ctx: { pm: PMDetails }, task: any) => { + ctx.pm = await resolvePackageManager(); + task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; + }, + }, + { + title: 'Configuring Yarn (if applicable)', + task: async ({ pm }: { pm: PMDetails }) => { + // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config + // lets ensure that nodeLinker is set to node-modules + if (pm.executable === 'yarn') { + const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); + if (!(await fs.pathExists(yarnrcPath))) { + d('creating .yarnrc.yml with nodeLinker: node-modules'); + await fs.writeFile( + yarnrcPath, + 'nodeLinker: node-modules\n', + ); + } + } + }, + }, + { + title: 'Installing dependencies', + task: async ({ pm }: { pm: PMDetails }, task: any) => { + await writeChanges(); + + d('deleting old dependencies forcefully'); + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron'), + ); + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron.cmd'), + ); + + d('installing dependencies'); + task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; + await installDependencies(pm, dir, importDeps); + + d('installing devDependencies'); + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; + 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 installDependencies( + pm, + dir, + importExactDevDeps, + DepType.DEV, + DepVersionRestriction.EXACT, + ); + }, + }, + { + title: 'Copying base template Forge configuration', + task: async () => { + const pathToTemplateConfig = path.resolve( + baseTemplate.templateDir, + 'forge.config.js', + ); + + // if there's an existing config.forge object in package.json + if ( + packageJSON?.config?.forge && + typeof packageJSON.config.forge === 'object' + ) { + d( + 'detected existing Forge config in package.json, merging with base template Forge config', + ); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const templateConfig = require( + path.resolve(baseTemplate.templateDir, 'forge.config.js'), + ); + packageJSON = await readRawPackageJson(dir); + merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object + await writeChanges(); + // otherwise, write to forge.config.js + } else { + d('writing new forge.config.js'); + await fs.copyFile( + pathToTemplateConfig, + path.resolve(dir, 'forge.config.js'), + ); + } + }, + }, + { + title: 'Fixing .gitignore', + task: async () => { + if (await fs.pathExists(path.resolve(dir, '.gitignore'))) { + const gitignore = await fs.readFile( + path.resolve(dir, '.gitignore'), + ); + if (!gitignore.includes(calculatedOutDir)) { + await fs.writeFile( + path.resolve(dir, '.gitignore'), + `${gitignore}\n${calculatedOutDir}/`, + ); + } + } + }, + }, + ], + listrOptions, + ); + }, + }, + { + title: 'Finalizing import', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: (_: any, task: any) => { + task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. + + Thanks for using ${chalk.green('Electron Forge')}!`; + }, + }, + ], + listrOptions, + ); + + await runner.run(); +} diff --git a/packages/external/create-electron-app/src/index.ts b/packages/external/create-electron-app/src/index.ts index 0cb57e085f..f44db99539 100644 --- a/packages/external/create-electron-app/src/index.ts +++ b/packages/external/create-electron-app/src/index.ts @@ -1,4 +1,39 @@ #!/usr/bin/env node +import chalk from 'chalk'; -/* eslint-disable */ -import '@electron-forge/cli/dist/electron-forge-init'; +function redConsoleError(msg: string) { + console.error(chalk.red(msg)); +} + +process.on( + 'unhandledRejection', + (reason: string, promise: Promise) => { + redConsoleError('\nAn unhandled rejection has occurred inside Forge:'); + redConsoleError(reason.toString().trim()); + promise.catch((err: Error) => { + if ('stack' in err) { + const usefulStack = err.stack; + if (usefulStack?.startsWith(reason.toString().trim())) { + redConsoleError( + usefulStack.substring(reason.toString().trim().length + 1).trim(), + ); + } + } + process.exit(1); + }); + }, +); + +process.on('uncaughtException', (err) => { + if (err && err.message && err.stack) { + redConsoleError('\nAn unhandled exception has occurred inside Forge:'); + redConsoleError(err.message); + redConsoleError(err.stack); + } else { + redConsoleError('\nElectron Forge was terminated:'); + redConsoleError(typeof err === 'string' ? err : JSON.stringify(err)); + } + process.exit(1); +}); + +import './init'; diff --git a/packages/api/core/src/api/init-scripts/find-template.ts b/packages/external/create-electron-app/src/init-scripts/find-template.ts similarity index 95% rename from packages/api/core/src/api/init-scripts/find-template.ts rename to packages/external/create-electron-app/src/init-scripts/find-template.ts index f4bcd15db1..a7cff68ed5 100644 --- a/packages/api/core/src/api/init-scripts/find-template.ts +++ b/packages/external/create-electron-app/src/init-scripts/find-template.ts @@ -1,8 +1,7 @@ +import { PossibleModule } from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; import debug from 'debug'; -import { PossibleModule } from '../../util/import-search'; - const d = debug('electron-forge:init:find-template'); export interface ForgeTemplateDetails { diff --git a/packages/api/core/src/api/init-scripts/init-directory.ts b/packages/external/create-electron-app/src/init-scripts/init-directory.ts similarity index 100% rename from packages/api/core/src/api/init-scripts/init-directory.ts rename to packages/external/create-electron-app/src/init-scripts/init-directory.ts diff --git a/packages/api/core/src/api/init-scripts/init-git.ts b/packages/external/create-electron-app/src/init-scripts/init-git.ts similarity index 100% rename from packages/api/core/src/api/init-scripts/init-git.ts rename to packages/external/create-electron-app/src/init-scripts/init-git.ts diff --git a/packages/api/core/src/api/init-scripts/init-link.ts b/packages/external/create-electron-app/src/init-scripts/init-link.ts similarity index 94% rename from packages/api/core/src/api/init-scripts/init-link.ts rename to packages/external/create-electron-app/src/init-scripts/init-link.ts index 140685d9ce..bc24c55ef9 100644 --- a/packages/api/core/src/api/init-scripts/init-link.ts +++ b/packages/external/create-electron-app/src/init-scripts/init-link.ts @@ -6,8 +6,6 @@ import { PMDetails, spawnPackageManager } from '@electron-forge/core-utils'; import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; -import { readRawPackageJson } from '../../util/read-package-json'; - const d = debug('electron-forge:init:link'); /** @@ -26,16 +24,12 @@ export async function initLink( const shouldLink = process.env.LINK_FORGE_DEPENDENCIES_ON_INIT; if (shouldLink) { d('Linking forge dependencies'); - const packageJson = await readRawPackageJson(dir); - const forgeRoot = path.resolve( - __dirname, - '..', - '..', - '..', - '..', - '..', - '..', + const raw = await fs.promises.readFile( + path.join(dir, 'package.json'), + 'utf-8', ); + const packageJson = JSON.parse(raw); + const forgeRoot = path.resolve(__dirname, '..', '..', '..', '..', '..'); const getWorkspacePath = (packageName: string): string => { const result = spawnSync( diff --git a/packages/api/core/src/api/init-scripts/init-npm.ts b/packages/external/create-electron-app/src/init-scripts/init-npm.ts similarity index 100% rename from packages/api/core/src/api/init-scripts/init-npm.ts rename to packages/external/create-electron-app/src/init-scripts/init-npm.ts diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/external/create-electron-app/src/init.ts similarity index 88% rename from packages/api/cli/src/electron-forge-init.ts rename to packages/external/create-electron-app/src/init.ts index a3dadb6e27..27aa67b2d3 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -1,16 +1,37 @@ import fs from 'node:fs'; +import path from 'node:path'; -import { api, InitOptions } from '@electron-forge/core'; import { confirm, select } from '@inquirer/prompts'; import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'; import chalk from 'chalk'; import { program } from 'commander'; import { Listr } from 'listr2'; -import './util/terminate'; import packageJSON from '../package.json'; -import { resolveWorkingDir } from './util/resolve-working-dir'; +import { init, InitOptions } from './core'; + +/** + * Resolves the directory in which to use a CLI command. + * @param dir - The directory specified by the user (can be relative or absolute) + * @param checkExisting - Checks if the directory exists. If true and directory is non-existent, it will fall back to the current working directory + * @returns + */ +export function resolveWorkingDir(dir: string, checkExisting = true): string { + if (!dir) { + return process.cwd(); + } + + const resolved = path.isAbsolute(dir) + ? dir + : path.resolve(process.cwd(), dir); + + if (checkExisting && !fs.existsSync(resolved)) { + return process.cwd(); + } else { + return resolved; + } +} // eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` import type { Prompt } from '@inquirer/type'; @@ -173,7 +194,7 @@ program ); const initOpts: InitOptions = await tasks.run(); - await api.init(initOpts); + await init(initOpts); }); program.parse(process.argv); diff --git a/packages/external/create-electron-app/src/util/resolve-working-dir.ts b/packages/external/create-electron-app/src/util/resolve-working-dir.ts new file mode 100644 index 0000000000..6438d023cb --- /dev/null +++ b/packages/external/create-electron-app/src/util/resolve-working-dir.ts @@ -0,0 +1,24 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * Resolves the directory in which to use a CLI command. + * @param dir - The directory specified by the user (can be relative or absolute) + * @param checkExisting - Checks if the directory exists. If true and directory is non-existent, it will fall back to the current working directory + * @returns + */ +export function resolveWorkingDir(dir: string, checkExisting = true): string { + if (!dir) { + return process.cwd(); + } + + const resolved = path.isAbsolute(dir) + ? dir + : path.resolve(process.cwd(), dir); + + if (checkExisting && !fs.existsSync(resolved)) { + return process.cwd(); + } else { + return resolved; + } +} diff --git a/packages/utils/core-utils/helper/dynamic-import.d.ts b/packages/utils/core-utils/helper/dynamic-import.d.ts new file mode 100644 index 0000000000..4d25e1299a --- /dev/null +++ b/packages/utils/core-utils/helper/dynamic-import.d.ts @@ -0,0 +1,3 @@ +export declare function dynamicImport(path: string): Promise; +/** Like {@link dynamicImport()}, except it tries out {@link require()} first. */ +export declare function dynamicImportMaybe(path: string): Promise; diff --git a/packages/utils/core-utils/helper/dynamic-import.js b/packages/utils/core-utils/helper/dynamic-import.js new file mode 100644 index 0000000000..728f341307 --- /dev/null +++ b/packages/utils/core-utils/helper/dynamic-import.js @@ -0,0 +1,23 @@ +const fs = require('fs'); +const url = require('url'); + +exports.dynamicImport = async function dynamicImport(path) { + try { + return await import(fs.existsSync(path) ? url.pathToFileURL(path) : path); + } catch (error) { + return Promise.reject(error); + } +}; + +exports.dynamicImportMaybe = async function dynamicImportMaybe(path) { + try { + return require(path); + } catch (e1) { + try { + return await exports.dynamicImport(path); + } catch (e2) { + e1.message = '\n1. ' + e1.message + '\n2. ' + e2.message; + throw e1; + } + } +}; diff --git a/packages/utils/core-utils/package.json b/packages/utils/core-utils/package.json index bdc839df89..e65db8a9f5 100644 --- a/packages/utils/core-utils/package.json +++ b/packages/utils/core-utils/package.json @@ -27,6 +27,7 @@ }, "files": [ "dist", + "helper", "src" ] } diff --git a/packages/api/core/spec/fixture/require-search/throw-error.js b/packages/utils/core-utils/spec/fixture/require-search/throw-error.js similarity index 100% rename from packages/api/core/spec/fixture/require-search/throw-error.js rename to packages/utils/core-utils/spec/fixture/require-search/throw-error.js diff --git a/packages/utils/core-utils/spec/import-search.spec.ts b/packages/utils/core-utils/spec/import-search.spec.ts new file mode 100644 index 0000000000..d93634598e --- /dev/null +++ b/packages/utils/core-utils/spec/import-search.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; + +import * as authorName from '../src/author-name'; +import { importSearch, importSearchRaw } from '../src/import-search'; + +describe('import-search', () => { + it('should resolve null if no file exists', async () => { + const resolved = await importSearch(__dirname, ['../src/wizard-secrets']); + expect(resolved).toEqual(null); + }); + + it('should resolve a file if it exists', async () => { + const resolved = await importSearchRaw(__dirname, ['../src/author-name']); + expect(resolved).toEqual(authorName); + }); + + it('should throw if file exists but fails to load', async () => { + await expect( + importSearch(__dirname, ['./fixture/require-search/throw-error']), + ).rejects.toThrowError('test'); + }); +}); diff --git a/packages/api/core/src/util/import-search.ts b/packages/utils/core-utils/src/import-search.ts similarity index 89% rename from packages/api/core/src/util/import-search.ts rename to packages/utils/core-utils/src/import-search.ts index ee8e975b3e..f6cb81bfdf 100644 --- a/packages/api/core/src/util/import-search.ts +++ b/packages/utils/core-utils/src/import-search.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import debug from 'debug'; // eslint-disable-next-line n/no-missing-import -import { dynamicImportMaybe } from '../../helper/dynamic-import.js'; +import { dynamicImportMaybe } from '../helper/dynamic-import.js'; const d = debug('electron-forge:import-search'); @@ -20,18 +20,18 @@ export async function importSearchRaw( ): Promise { // Attempt to locally short-circuit if we're running from a checkout of forge if ( - __dirname.includes('forge/packages/api/core/') && + __dirname.includes('forge/packages/') && paths.length === 1 && paths[0].startsWith('@electron-forge/') ) { const [moduleType, moduleName] = paths[0].split('/')[1].split('-'); try { + // From packages/utils/core-utils/dist (or src), resolve up to packages/ const localPath = path.resolve( __dirname, '..', '..', '..', - '..', moduleType, moduleName, ); @@ -74,12 +74,12 @@ export type PossibleModule = { default?: T; } & T; -export default async ( +export async function importSearch( relativeTo: string, paths: string[], -): Promise => { +): Promise { const result = await importSearchRaw>(relativeTo, paths); return typeof result === 'object' && result && result.default ? result.default : (result as T | null); -}; +} diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index 2c1195fa85..3d240de948 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -3,3 +3,4 @@ export * from './electron-version'; export * from './package-manager'; export * from './author-name'; export * from './install-dependencies'; +export * from './import-search'; From 93c7ac6bac32e7d17bfb8adb9bc1cdc7b9f287d1 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 14:45:51 -0800 Subject: [PATCH 03/27] fixup: delete duplicate files --- .../api/core/spec/fast/find-template.spec.ts | 51 ------------------- .../core/spec/fast/util/import-search.spec.ts | 26 ---------- 2 files changed, 77 deletions(-) delete mode 100644 packages/api/core/spec/fast/find-template.spec.ts delete mode 100644 packages/api/core/spec/fast/util/import-search.spec.ts diff --git a/packages/api/core/spec/fast/find-template.spec.ts b/packages/api/core/spec/fast/find-template.spec.ts deleted file mode 100644 index 6f05c334f1..0000000000 --- a/packages/api/core/spec/fast/find-template.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { findTemplate } from '../../src/api/init-scripts/find-template'; - -describe('findTemplate', () => { - /** - * Note: this test suite does not mock `require.resolve`. Instead, it uses - * fixture dependencies defined in this module's package.json file to - * actually resolve a local template. - * - * If you modify the fixtures, you may need to re-run `yarn install` in order - * for the fixtures to be installed in your local `node_modules`. - */ - describe('local modules', () => { - it('should find an @electron-forge/template based on partial name', async () => { - await expect(findTemplate('fixture')).resolves.toEqual( - expect.objectContaining({ name: '@electron-forge/template-fixture' }), - ); - }); - - it('should find an @electron-forge/template based on full name', async () => { - await expect( - findTemplate('@electron-forge/template-fixture'), - ).resolves.toEqual( - expect.objectContaining({ name: '@electron-forge/template-fixture' }), - ); - }); - it('should find an electron-forge-template based on partial name', async () => { - await expect(findTemplate('fixture-two')).resolves.toEqual( - expect.objectContaining({ - name: 'electron-forge-template-fixture-two', - }), - ); - }); - it('should find an @electron-forge-template based on full name', async () => { - await expect( - findTemplate('electron-forge-template-fixture-two'), - ).resolves.toEqual( - expect.objectContaining({ - name: 'electron-forge-template-fixture-two', - }), - ); - }); - }); - - it('should error if there are no valid templates', async () => { - await expect(findTemplate('non-existent-template')).rejects.toThrowError( - 'Failed to locate custom template: "non-existent-template".', - ); - }); -}); diff --git a/packages/api/core/spec/fast/util/import-search.spec.ts b/packages/api/core/spec/fast/util/import-search.spec.ts deleted file mode 100644 index f7fb93f094..0000000000 --- a/packages/api/core/spec/fast/util/import-search.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import findConfig from '../../../src/util/forge-config'; -import importSearch from '../../../src/util/import-search'; - -describe('import-search', () => { - it('should resolve null if no file exists', async () => { - const resolved = await importSearch(__dirname, [ - '../../../src/util/wizard-secrets', - ]); - expect(resolved).toEqual(null); - }); - - it('should resolve a file if it exists', async () => { - const resolved = await importSearch(__dirname, [ - '../../../src/util/forge-config', - ]); - expect(resolved).toEqual(findConfig); - }); - - it('should throw if file exists but fails to load', async () => { - await expect( - importSearch(__dirname, ['../../fixture/require-search/throw-error']), - ).rejects.toThrowError('test'); - }); -}); From 5461185c100f666bdb4b700137808217222979f3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 15:38:04 -0800 Subject: [PATCH 04/27] fix up fast tests --- .../core/spec/fast/util/import-search.spec.ts | 26 +++++++++++++++++++ .../fixture/require-search/throw-error.js | 0 packages/api/core/src/api/make.ts | 3 ++- .../core/src/util}/import-search.ts | 2 +- .../src/init-scripts/init-npm.ts | 6 ++--- .../core-utils/spec/electron-version.spec.ts | 2 +- .../core-utils/spec/import-search.spec.ts | 22 ---------------- packages/utils/core-utils/src/index.ts | 1 - 8 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 packages/api/core/spec/fast/util/import-search.spec.ts rename packages/{utils/core-utils => api/core}/spec/fixture/require-search/throw-error.js (100%) rename packages/{utils/core-utils/src => api/core/src/util}/import-search.ts (97%) delete mode 100644 packages/utils/core-utils/spec/import-search.spec.ts diff --git a/packages/api/core/spec/fast/util/import-search.spec.ts b/packages/api/core/spec/fast/util/import-search.spec.ts new file mode 100644 index 0000000000..a4f12aea2c --- /dev/null +++ b/packages/api/core/spec/fast/util/import-search.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import findConfig from '../../../src/util/forge-config'; +import { importSearch } from '../../../src/util/import-search'; + +describe('import-search', () => { + it('should resolve null if no file exists', async () => { + const resolved = await importSearch(__dirname, [ + '../../../src/util/wizard-secrets', + ]); + expect(resolved).toEqual(null); + }); + + it('should resolve a file if it exists', async () => { + const resolved = await importSearch(__dirname, [ + '../../../src/util/forge-config', + ]); + expect(resolved).toEqual(findConfig); + }); + + it('should throw if file exists but fails to load', async () => { + await expect( + importSearch(__dirname, ['../../fixture/require-search/throw-error']), + ).rejects.toThrowError('test'); + }); +}); diff --git a/packages/utils/core-utils/spec/fixture/require-search/throw-error.js b/packages/api/core/spec/fixture/require-search/throw-error.js similarity index 100% rename from packages/utils/core-utils/spec/fixture/require-search/throw-error.js rename to packages/api/core/spec/fixture/require-search/throw-error.js diff --git a/packages/api/core/src/api/make.ts b/packages/api/core/src/api/make.ts index 4136d3e108..3720627014 100644 --- a/packages/api/core/src/api/make.ts +++ b/packages/api/core/src/api/make.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { getHostArch } from '@electron/get'; -import { getElectronVersion, importSearch } from '@electron-forge/core-utils'; +import { getElectronVersion } from '@electron-forge/core-utils'; import { MakerBase } from '@electron-forge/maker-base'; import { ForgeArch, @@ -22,6 +22,7 @@ import logSymbols from 'log-symbols'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runMutatingHook } from '../util/hook'; +import { importSearch } from '../util/import-search'; import getCurrentOutDir from '../util/out-dir'; import parseArchs from '../util/parse-archs'; import { readMutatedPackageJson } from '../util/read-package-json'; diff --git a/packages/utils/core-utils/src/import-search.ts b/packages/api/core/src/util/import-search.ts similarity index 97% rename from packages/utils/core-utils/src/import-search.ts rename to packages/api/core/src/util/import-search.ts index f6cb81bfdf..a3564d2261 100644 --- a/packages/utils/core-utils/src/import-search.ts +++ b/packages/api/core/src/util/import-search.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import debug from 'debug'; // eslint-disable-next-line n/no-missing-import -import { dynamicImportMaybe } from '../helper/dynamic-import.js'; +import { dynamicImportMaybe } from '../../helper/dynamic-import.js'; const d = debug('electron-forge:import-search'); diff --git a/packages/external/create-electron-app/src/init-scripts/init-npm.ts b/packages/external/create-electron-app/src/init-scripts/init-npm.ts index f540493222..951b777484 100644 --- a/packages/external/create-electron-app/src/init-scripts/init-npm.ts +++ b/packages/external/create-electron-app/src/init-scripts/init-npm.ts @@ -12,12 +12,12 @@ import fs from 'fs-extra'; import semver from 'semver'; const d = debug('electron-forge:init:npm'); -const corePackage = fs.readJsonSync( - path.resolve(__dirname, '../../../package.json'), +const packageJSON = fs.readJsonSync( + path.resolve(__dirname, '../../package.json'), ); export function siblingDep(name: string): string { - return `@electron-forge/${name}@^${corePackage.version}`; + return `@electron-forge/${name}@^${packageJSON.version}`; } export const deps = ['electron-squirrel-startup']; diff --git a/packages/utils/core-utils/spec/electron-version.spec.ts b/packages/utils/core-utils/spec/electron-version.spec.ts index c724cc8932..43169994a0 100644 --- a/packages/utils/core-utils/spec/electron-version.spec.ts +++ b/packages/utils/core-utils/spec/electron-version.spec.ts @@ -8,7 +8,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { devDeps, exactDevDeps, -} from '../../../api/core/src/api/init-scripts/init-npm'; +} from '../../../external/create-electron-app/src/init-scripts/init-npm'; import { getElectronModulePath, getElectronVersion, diff --git a/packages/utils/core-utils/spec/import-search.spec.ts b/packages/utils/core-utils/spec/import-search.spec.ts deleted file mode 100644 index d93634598e..0000000000 --- a/packages/utils/core-utils/spec/import-search.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import * as authorName from '../src/author-name'; -import { importSearch, importSearchRaw } from '../src/import-search'; - -describe('import-search', () => { - it('should resolve null if no file exists', async () => { - const resolved = await importSearch(__dirname, ['../src/wizard-secrets']); - expect(resolved).toEqual(null); - }); - - it('should resolve a file if it exists', async () => { - const resolved = await importSearchRaw(__dirname, ['../src/author-name']); - expect(resolved).toEqual(authorName); - }); - - it('should throw if file exists but fails to load', async () => { - await expect( - importSearch(__dirname, ['./fixture/require-search/throw-error']), - ).rejects.toThrowError('test'); - }); -}); diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index 3d240de948..2c1195fa85 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -3,4 +3,3 @@ export * from './electron-version'; export * from './package-manager'; export * from './author-name'; export * from './install-dependencies'; -export * from './import-search'; From 4919ce5e271b7cce12a1a9e257a2a0dc39436a3e Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 15:48:23 -0800 Subject: [PATCH 05/27] types fixups --- packages/api/core/src/api/package.ts | 2 +- packages/api/core/src/api/publish.ts | 2 +- packages/api/core/src/util/import-search.ts | 5 +---- packages/api/core/src/util/plugin-interface.ts | 2 +- .../create-electron-app/src/init-scripts/find-template.ts | 3 +-- packages/utils/types/src/index.ts | 4 ++++ 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/api/core/src/api/package.ts b/packages/api/core/src/api/package.ts index 560ed3d26f..fb9534b303 100644 --- a/packages/api/core/src/api/package.ts +++ b/packages/api/core/src/api/package.ts @@ -11,7 +11,6 @@ import { } from '@electron/packager'; import { getElectronVersion, - importSearch, listrCompatibleRebuildHook, } from '@electron-forge/core-utils'; import { @@ -31,6 +30,7 @@ import { Listr, PRESET_TIMER } from 'listr2'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runHook } from '../util/hook'; +import { importSearch } from '../util/import-search'; import { warn } from '../util/messages'; import getCurrentOutDir from '../util/out-dir'; import { readMutatedPackageJson } from '../util/read-package-json'; diff --git a/packages/api/core/src/api/publish.ts b/packages/api/core/src/api/publish.ts index 20df46a19c..4a4cb811b7 100644 --- a/packages/api/core/src/api/publish.ts +++ b/packages/api/core/src/api/publish.ts @@ -1,6 +1,5 @@ import path from 'node:path'; -import { importSearch } from '@electron-forge/core-utils'; import { PublisherBase } from '@electron-forge/publisher-base'; import { ForgeConfigPublisher, @@ -20,6 +19,7 @@ import fs from 'fs-extra'; import { Listr } from 'listr2'; import getForgeConfig from '../util/forge-config'; +import { importSearch } from '../util/import-search'; import getCurrentOutDir from '../util/out-dir'; import PublishState from '../util/publish-state'; import resolveDir from '../util/resolve-dir'; diff --git a/packages/api/core/src/util/import-search.ts b/packages/api/core/src/util/import-search.ts index a3564d2261..d62a0040e6 100644 --- a/packages/api/core/src/util/import-search.ts +++ b/packages/api/core/src/util/import-search.ts @@ -1,5 +1,6 @@ import path from 'node:path'; +import { PossibleModule } from '@electron-forge/shared-types'; import debug from 'debug'; // eslint-disable-next-line n/no-missing-import @@ -70,10 +71,6 @@ export async function importSearchRaw( return null; } -export type PossibleModule = { - default?: T; -} & T; - export async function importSearch( relativeTo: string, paths: string[], diff --git a/packages/api/core/src/util/plugin-interface.ts b/packages/api/core/src/util/plugin-interface.ts index e9f0063e53..d15fbb802c 100644 --- a/packages/api/core/src/util/plugin-interface.ts +++ b/packages/api/core/src/util/plugin-interface.ts @@ -1,4 +1,3 @@ -import { importSearch } from '@electron-forge/core-utils'; import { PluginBase } from '@electron-forge/plugin-base'; import { ForgeListrTaskDefinition, @@ -17,6 +16,7 @@ import debug from 'debug'; // eslint-disable-next-line n/no-missing-import import { StartOptions } from '../api'; +import { importSearch } from '../util/import-search'; const d = debug('electron-forge:plugins'); diff --git a/packages/external/create-electron-app/src/init-scripts/find-template.ts b/packages/external/create-electron-app/src/init-scripts/find-template.ts index a7cff68ed5..855b2a009b 100644 --- a/packages/external/create-electron-app/src/init-scripts/find-template.ts +++ b/packages/external/create-electron-app/src/init-scripts/find-template.ts @@ -1,5 +1,4 @@ -import { PossibleModule } from '@electron-forge/core-utils'; -import { ForgeTemplate } from '@electron-forge/shared-types'; +import { ForgeTemplate, PossibleModule } from '@electron-forge/shared-types'; import debug from 'debug'; const d = debug('electron-forge:init:find-template'); diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index 52ce10fd66..e8c1781897 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -285,3 +285,7 @@ export type PackagePerson = email?: string; url?: string; }; + +export type PossibleModule = { + default?: T; +} & T; From 672172fd3801f8c5a65ac7e7f7794f7b936bb17d Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 15:54:08 -0800 Subject: [PATCH 06/27] ??? --- tools/verdaccio/spawn-verdaccio.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/verdaccio/spawn-verdaccio.ts b/tools/verdaccio/spawn-verdaccio.ts index 07f59d2970..435c080d43 100644 --- a/tools/verdaccio/spawn-verdaccio.ts +++ b/tools/verdaccio/spawn-verdaccio.ts @@ -133,6 +133,7 @@ async function publishPackages(): Promise { } async function runCommand(args: string[]) { + process.env.COREPACK_ENABLE_STRICT = '0'; console.log('🗑️ Pruning pnpm store before running command'); await spawnPromise('pnpm', ['store', 'prune']); From e612cec6fe097fbcc422355c7aa6216363084d04 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 24 Feb 2026 16:02:36 -0800 Subject: [PATCH 07/27] fix verdaccio test --- .../spec/slow/init.slow.verdaccio.spec.ts | 34 +++++++++---------- .../external/create-electron-app/src/core.ts | 2 +- ...eTypeScriptTemplate.slow.verdaccio.spec.ts | 3 +- .../WebpackTypeScript.slow.verdaccio.spec.ts | 3 +- yarn.lock | 26 ++++++++++---- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts index 0c3e963283..00ddc4fe67 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts @@ -8,7 +8,7 @@ import { import semver from 'semver'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { api } from '../../src/api/index'; +import { init } from '../../../../external/create-electron-app/src/core'; describe('init', () => { let dir: string; @@ -21,7 +21,7 @@ describe('init', () => { }); it('works (base case)', async () => { - await api.init({ + await init({ dir, }); expect(fs.existsSync(dir)).toEqual(true); @@ -51,7 +51,7 @@ describe('init', () => { describe('with electronVersion', () => { it('can define a specific Electron version with a version number', async () => { - await api.init({ + await init({ dir, electronVersion: 'v38.0.0', }); @@ -60,7 +60,7 @@ describe('init', () => { }); it('can define a specific Electron nightly version with a version number', async () => { - await api.init({ + await init({ dir, electronVersion: '40.0.0-nightly.20251020', }); @@ -72,7 +72,7 @@ describe('init', () => { }); it('can define a specific Electron prerelease version with the beta tag', async () => { - await api.init({ + await init({ dir, electronVersion: 'beta', }); @@ -86,7 +86,7 @@ describe('init', () => { }); it('can define a specific Electron nightly version with the nightly tag', async () => { - await api.init({ + await init({ dir, electronVersion: 'nightly', }); @@ -100,7 +100,7 @@ describe('init', () => { describe('with skipGit', () => { it('should not initialize a git repo if passed the skipGit option', async () => { - await api.init({ + await init({ dir, skipGit: true, }); @@ -110,7 +110,7 @@ describe('init', () => { describe('with custom template', () => { it('adds all files correctly', async () => { - await api.init({ + await init({ dir, template: path.resolve(__dirname, '../fixture/custom_init'), }); @@ -145,7 +145,7 @@ describe('init', () => { describe('without a required Forge version)', () => { it('should fail in initializing', async () => { await expect( - api.init({ + init({ dir, template: path.resolve( __dirname, @@ -159,7 +159,7 @@ describe('init', () => { describe('with a non-matching Forge version', () => { it('should fail in initializing', async () => { await expect( - api.init({ + init({ dir, template: path.resolve( __dirname, @@ -175,7 +175,7 @@ describe('init', () => { describe('with a nonexistent template', () => { it('should fail in initializing', async () => { await expect( - api.init({ + init({ dir, template: 'does-not-exist', }), @@ -189,20 +189,20 @@ describe('init', () => { let persistentDir: string; beforeAll(async () => { persistentDir = await ensureTestDirIsNonexistent(); - await api.init({ dir: persistentDir }); + await init({ dir: persistentDir }); return async () => { await fs.promises.rm(persistentDir, { recursive: true, force: true }); }; }); it('should fail without the force flag', async () => { - await expect(api.init({ dir: persistentDir })).rejects.toThrow( + await expect(init({ dir: persistentDir })).rejects.toThrow( `The specified path: "${persistentDir}" is not empty. Please ensure it is empty before initializing a new project`, ); }); it('should pass with the force flag', async () => { - await api.init({ + await init({ dir: persistentDir, force: true, }); @@ -218,7 +218,7 @@ describe('init', () => { }); it('initializes with package-lock.json', async () => { - await api.init({ dir, packageManager: 'npm' }); + await init({ dir, packageManager: 'npm' }); expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBe(true); expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBe(false); @@ -230,7 +230,7 @@ describe('init', () => { // due to the `packageManager` entry in this monorepo. describe('with yarn (berry)', () => { it('initializes with correct nodeLinker value', async () => { - await api.init({ dir, packageManager: 'yarn@4.10.3' }); + await init({ dir, packageManager: 'yarn@4.10.3' }); expect( fs.readFileSync(path.join(dir, '.yarnrc.yml'), 'utf-8'), @@ -261,7 +261,7 @@ describe('init', () => { }); it('initializes with correct node-linker value', async () => { - await api.init({ dir, packageManager: 'pnpm' }); + await init({ dir, packageManager: 'pnpm' }); expect(fs.readFileSync(path.join(dir, '.npmrc'), 'utf-8')).toContain( 'node-linker = hoisted', diff --git a/packages/external/create-electron-app/src/core.ts b/packages/external/create-electron-app/src/core.ts index 63ee896eea..352c866462 100644 --- a/packages/external/create-electron-app/src/core.ts +++ b/packages/external/create-electron-app/src/core.ts @@ -71,7 +71,7 @@ async function validateTemplate( ); } - const dir = path.join(__dirname, '..', '..'); + const dir = path.join(__dirname, '..'); const raw = await fs.promises.readFile( path.join(dir, 'package.json'), 'utf-8', diff --git a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts index 6ea8e69950..0a24e913f6 100644 --- a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts +++ b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts @@ -12,6 +12,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; // eslint-disable-next-line n/no-missing-import import { api } from '../../../api/core/dist/api'; +import { init } from '../../../external/create-electron-app/src/core'; describe('ViteTypeScriptTemplate', () => { let dir: string; @@ -30,7 +31,7 @@ describe('ViteTypeScriptTemplate', () => { describe('template files are copied to project', () => { it('should succeed in initializing the typescript template', async () => { - await api.init({ + await init({ dir, template: path.resolve(__dirname, '..'), interactive: false, diff --git a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts index 32383ca7af..0af895d128 100644 --- a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts +++ b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts @@ -11,6 +11,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; // eslint-disable-next-line n/no-missing-import import { api } from '../../../api/core/dist/api'; +import { init } from '../../../external/create-electron-app/src/core'; describe('WebpackTypeScriptTemplate', () => { let dir: string; @@ -20,7 +21,7 @@ describe('WebpackTypeScriptTemplate', () => { }); it('should succeed in initializing the typescript template', async () => { - await api.init({ + await init({ dir, template: path.join(__dirname, '..'), interactive: false, diff --git a/yarn.lock b/yarn.lock index 6e76aeceec..f8482f356e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -805,7 +805,7 @@ __metadata: languageName: node linkType: hard -"@electron-forge/cli@workspace:*, @electron-forge/cli@workspace:packages/api/cli": +"@electron-forge/cli@workspace:packages/api/cli": version: 0.0.0-use.local resolution: "@electron-forge/cli@workspace:packages/api/cli" dependencies: @@ -867,12 +867,7 @@ __metadata: "@electron-forge/plugin-base": "workspace:*" "@electron-forge/publisher-base": "workspace:*" "@electron-forge/shared-types": "workspace:*" - "@electron-forge/template-base": "workspace:*" "@electron-forge/template-fixture": "link:./spec/fixture/electron-forge-template-fixture" - "@electron-forge/template-vite": "workspace:*" - "@electron-forge/template-vite-typescript": "workspace:*" - "@electron-forge/template-webpack": "workspace:*" - "@electron-forge/template-webpack-typescript": "workspace:*" "@electron-forge/test-utils": "workspace:*" "@electron-forge/tracer": "workspace:*" "@electron/get": "npm:^3.0.0" @@ -8007,7 +8002,24 @@ __metadata: version: 0.0.0-use.local resolution: "create-electron-app@workspace:packages/external/create-electron-app" dependencies: - "@electron-forge/cli": "workspace:*" + "@electron-forge/core-utils": "workspace:*" + "@electron-forge/shared-types": "workspace:*" + "@electron-forge/template-base": "workspace:*" + "@electron-forge/template-vite": "workspace:*" + "@electron-forge/template-vite-typescript": "workspace:*" + "@electron-forge/template-webpack": "workspace:*" + "@electron-forge/template-webpack-typescript": "workspace:*" + "@inquirer/prompts": "npm:^6.0.1" + "@listr2/prompt-adapter-inquirer": "npm:^2.0.22" + "@malept/cross-spawn-promise": "npm:^2.0.0" + chalk: "npm:^4.0.0" + commander: "npm:^11.1.0" + debug: "npm:^4.3.1" + fs-extra: "npm:^10.0.0" + listr2: "npm:^7.0.2" + lodash: "npm:^4.17.20" + log-symbols: "npm:^4.0.0" + semver: "npm:^7.2.1" bin: create-electron-app: dist/index.js languageName: unknown From 6301fceaf0a9f4dc6175ffd30f54da7c467ba1a7 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 25 Feb 2026 09:28:45 -0800 Subject: [PATCH 08/27] fix publish tests --- packages/api/core/spec/fast/publish.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/core/spec/fast/publish.spec.ts b/packages/api/core/spec/fast/publish.spec.ts index a94322e078..e70f5eba37 100644 --- a/packages/api/core/spec/fast/publish.spec.ts +++ b/packages/api/core/spec/fast/publish.spec.ts @@ -2,7 +2,6 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { importSearch } from '@electron-forge/core-utils'; import { ForgeMakeResult, ResolvedForgeConfig, @@ -12,6 +11,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { listrMake } from '../../src/api/make'; import publish from '../../src/api/publish'; import findConfig from '../../src/util/forge-config'; +import { importSearch } from '../../src/util/import-search'; vi.mock(import('../../src/api/make'), async (importOriginal) => { const mod = await importOriginal(); @@ -37,7 +37,7 @@ vi.mock(import('../../src/util/resolve-dir'), async (importOriginal) => { }; }); -vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { +vi.mock(import('../../src/util/import-search'), async (importOriginal) => { const mod = await importOriginal(); return { ...mod, From f7c81f9f91d09bfc8e85791863d008391ed8af76 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 25 Feb 2026 16:38:03 -0800 Subject: [PATCH 09/27] touchups --- packages/api/cli/src/electron-forge.ts | 1 - packages/external/create-electron-app/src/init.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 617ee3553a..771ab2da10 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -25,7 +25,6 @@ import { Listr } from 'listr2'; program .version(packageJSON.version, '-V, --version', 'Output the current version.') .helpOption('-h, --help', 'Output usage information.') - .command('init', 'Initialize a new Electron application.') .command( 'start', 'Start the current Electron application in development mode.', diff --git a/packages/external/create-electron-app/src/init.ts b/packages/external/create-electron-app/src/init.ts index 27aa67b2d3..78c7b7371d 100644 --- a/packages/external/create-electron-app/src/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -11,6 +11,9 @@ import packageJSON from '../package.json'; import { init, InitOptions } from './core'; +// eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` +import type { Prompt } from '@inquirer/type'; + /** * Resolves the directory in which to use a CLI command. * @param dir - The directory specified by the user (can be relative or absolute) @@ -33,9 +36,6 @@ export function resolveWorkingDir(dir: string, checkExisting = true): string { } } -// eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` -import type { Prompt } from '@inquirer/type'; - program .version(packageJSON.version, '-V, --version', 'Output the current version.') .helpOption('-h, --help', 'Output usage information.') From d75aee66ac145814d57b3729f272408af68aadaa Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 25 Feb 2026 21:20:58 -0800 Subject: [PATCH 10/27] reorganizing --- .../external/create-electron-app/src/core.ts | 278 ------------ .../src/create-electron-app.ts | 203 +++++++++ .../src/{import-core.ts => import.ts} | 0 .../external/create-electron-app/src/index.ts | 2 +- .../external/create-electron-app/src/init.ts | 420 +++++++++++------- 5 files changed, 453 insertions(+), 450 deletions(-) delete mode 100644 packages/external/create-electron-app/src/core.ts create mode 100644 packages/external/create-electron-app/src/create-electron-app.ts rename packages/external/create-electron-app/src/{import-core.ts => import.ts} (100%) diff --git a/packages/external/create-electron-app/src/core.ts b/packages/external/create-electron-app/src/core.ts deleted file mode 100644 index 352c866462..0000000000 --- a/packages/external/create-electron-app/src/core.ts +++ /dev/null @@ -1,278 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import { - DepType, - DepVersionRestriction, - installDependencies, - PMDetails, - resolvePackageManager, -} from '@electron-forge/core-utils'; -import { ForgeTemplate } from '@electron-forge/shared-types'; -import { spawn } from '@malept/cross-spawn-promise'; -import chalk from 'chalk'; -import debug from 'debug'; -import { Listr } from 'listr2'; -import semver from 'semver'; - -import { findTemplate } from './init-scripts/find-template'; -import { initDirectory } from './init-scripts/init-directory'; -import { initGit } from './init-scripts/init-git'; -import { initLink } from './init-scripts/init-link'; -import { initNPM } from './init-scripts/init-npm'; - -const d = debug('electron-forge:init'); - -export interface InitOptions { - /** - * The path to the app to be initialized - */ - dir?: string; - /** - * Whether to use sensible defaults or prompt the user visually - */ - interactive?: boolean; - /** - * Whether to copy template CI files - */ - copyCIFiles?: boolean; - /** - * Whether to overwrite an existing directory - */ - force?: boolean; - /** - * The custom template to use. If left empty, the default template is used - */ - template?: string; - /** - * 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; - /** - * Force a package manager to use (npm|yarn|pnpm). - */ - packageManager?: string; -} - -async function validateTemplate( - template: string, - templateModule: ForgeTemplate, -): Promise { - if (!templateModule.requiredForgeVersion) { - throw new Error( - `Cannot use a template (${template}) with this version of Electron Forge, as it does not specify its required Forge version.`, - ); - } - - const dir = path.join(__dirname, '..'); - const raw = await fs.promises.readFile( - path.join(dir, 'package.json'), - 'utf-8', - ); - const packageJSON = JSON.parse(raw); - - const forgeVersion = packageJSON.version; - if (!semver.satisfies(forgeVersion, templateModule.requiredForgeVersion)) { - throw new Error( - `Template (${template}) is not compatible with this version of Electron Forge (${forgeVersion}), it requires ${templateModule.requiredForgeVersion}`, - ); - } -} - -export async function init({ - dir = process.cwd(), - interactive = false, - copyCIFiles = false, - force = false, - template = 'base', - skipGit = false, - electronVersion = 'latest', - packageManager, -}: InitOptions): Promise { - d(`Initializing in: ${dir}`); - - const runner = new Listr<{ - templateModule: ForgeTemplate; - pm: PMDetails; - parsedElectronVersion: string; - }>( - [ - { - title: `Resolving package manager`, - task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(packageManager); - task.title = `Resolved package manager: ${chalk.cyan(`${ctx.pm.executable}@${ctx.pm.version}`)}`; - }, - }, - { - title: `Resolving template: ${chalk.cyan(template)}`, - task: async (ctx, task) => { - const tmpl = await findTemplate(template); - ctx.templateModule = tmpl.template; - task.output = `Using ${chalk.green(tmpl.name)}`; - }, - 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)}`; - ctx.parsedElectronVersion = electronVersion; - } else { - // 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 }, - }, - { - title: 'Initializing directory', - task: async (_, task) => { - await initDirectory(dir, task, force); - }, - rendererOptions: { persistentOutput: true }, - }, - { - title: 'Initializing git repository', - enabled: !skipGit, - task: async () => { - await initGit(dir); - }, - }, - { - title: 'Preparing template', - task: async ({ templateModule }) => { - await validateTemplate(template, templateModule); - }, - }, - { - title: `Initializing template`, - task: async ({ templateModule }, task) => { - if (typeof templateModule.initializeTemplate === 'function') { - const tasks = await templateModule.initializeTemplate(dir, { - copyCIFiles, - force, - }); - if (tasks) { - return task.newListr(tasks, { concurrent: false }); - } - } - }, - }, - { - title: `Setting package manager with Corepack`, - // pm.executable needs to be optional here because the code gets evaluated twice (on init and on execution) - // @see https://listr2.kilic.dev/task/enable.html - enabled: ({ pm }) => pm?.executable !== 'npm', - task: async ({ pm }, task) => { - const pmString = `${pm.executable}@${pm.version}`; - try { - await spawn('corepack', ['use', pmString], { - cwd: dir, - }); - task.title = `Set ${chalk.cyan(pmString)} via Corepack`; - } catch (e) { - d('corepack failed to run with error', e); - task.title = `Forge was unable to set ${chalk.cyan(pmString)} via Corepack and will fall back to ${chalk.cyan('npm')}. If you are using Node.js >= 25, you will need to install corepack via ${chalk.green('npm install -g corepack')}. Otherwise, you may need to enable Corepack shims via ${chalk.green('corepack enable')}.`; - } - }, - }, - { - title: 'Installing template dependencies', - task: async ({ templateModule }, task) => { - return task.newListr( - [ - { - title: 'Installing production dependencies', - task: async ({ pm }, task) => { - d('installing dependencies'); - if (templateModule.dependencies?.length) { - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.dependencies.join(' ')}`; - } - return await installDependencies( - pm, - dir, - templateModule.dependencies || [], - DepType.PROD, - DepVersionRestriction.RANGE, - ); - }, - exitOnError: false, - }, - { - title: 'Installing development dependencies', - task: async ({ pm }, task) => { - d('installing devDependencies'); - if (templateModule.devDependencies?.length) { - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.devDependencies.join(' ')}`; - } - await installDependencies( - pm, - dir, - templateModule.devDependencies || [], - DepType.DEV, - ); - }, - exitOnError: false, - }, - { - title: 'Finalizing dependencies', - task: async (ctx, task) => { - return task.newListr([ - { - title: 'Installing common dependencies', - task: async ({ pm }, task) => { - await initNPM(pm, dir, ctx.parsedElectronVersion, task); - }, - exitOnError: false, - }, - { - title: 'Linking Forge dependencies to local build', - enabled: !!process.env.LINK_FORGE_DEPENDENCIES_ON_INIT, - task: async ({ pm }, task) => { - await initLink(pm, dir, task); - }, - exitOnError: true, - }, - ]); - }, - }, - ], - { - concurrent: false, - }, - ); - }, - }, - ], - { - concurrent: false, - silentRendererCondition: !interactive, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), - }, - ); - - await runner.run(); -} diff --git a/packages/external/create-electron-app/src/create-electron-app.ts b/packages/external/create-electron-app/src/create-electron-app.ts new file mode 100644 index 0000000000..903dec8c4c --- /dev/null +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -0,0 +1,203 @@ +import fs from 'node:fs'; + +import { confirm, select } from '@inquirer/prompts'; +import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'; +import chalk from 'chalk'; +import { program } from 'commander'; +import { Listr } from 'listr2'; + +import packageJSON from '../package.json'; + +import { forgeImport } from './import'; +import { init, InitOptions } from './init'; +import { resolveWorkingDir } from './util/resolve-working-dir'; + +// eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` +import type { Prompt } from '@inquirer/type'; + +program + .version(packageJSON.version, '-V, --version', 'Output the current version.') + .helpOption('-h, --help', 'Output usage information.'); + +const initCommand = program + .command('init', { isDefault: true }) + .description('Initialize a new Electron Forge project.') + .argument( + '[dir]', + 'Directory to initialize the project in. Defaults to the current directory.', + ) + .option('-t, --template [name]', 'Name of the Forge template to use.') + .option('-c, --copy-ci-files', 'Whether to copy the templated CI files.') + .option('-f, --force', 'Whether to overwrite an existing directory.') + .option( + '--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` tags.', + ) + .option( + '--package-manager [name]', + 'Set a specific package manager to use for your Forge project. Supported package managers are `npm`, `pnpm`, and `yarn`. You can also specify an exact version to use (e.g. `yarn@1.22.22`).', + ) + .action(async (dir) => { + const options = initCommand.opts(); + const tasks = new Listr( + [ + { + task: async (initOpts): Promise => { + initOpts.interactive = true; + initOpts.template = options.template ?? 'base'; + initOpts.copyCIFiles = Boolean(options.copyCiFiles); + initOpts.force = Boolean(options.force); + initOpts.skipGit = Boolean(options.skipGit); + initOpts.dir = resolveWorkingDir(dir, false); + initOpts.electronVersion = options.electronVersion ?? 'latest'; + initOpts.packageManager = options.packageManager ?? 'npm@latest'; + }, + }, + { + task: async (initOpts, task): Promise => { + if ( + Object.keys(options).length > 0 || + process.env.CI || + !process.stdout.isTTY + ) { + return; + } + + const prompt = task.prompt(ListrInquirerPromptAdapter); + + if ( + typeof initOpts.dir === 'string' && + fs.existsSync(initOpts.dir) && + (await fs.promises.readdir(initOpts.dir)).length > 0 + ) { + const confirmResult = await prompt.run(confirm, { + message: `${chalk.cyan(initOpts.dir)} is not empty. Would you like to continue and overwrite existing files?`, + default: false, + }); + + if (confirmResult) { + initOpts.force = true; + } else { + task.output = 'Directory is not empty. Exiting.'; + process.exit(0); + } + } + + const packageManager: string = await prompt.run< + Prompt + >(select, { + message: 'Select a package manager', + choices: [ + { name: 'npm', value: 'npm@latest' }, + { name: 'pnpm', value: 'pnpm@latest' }, + { name: 'Yarn (Berry)', value: 'yarn@latest' }, + { name: 'Yarn (Classic)', value: 'yarn@1' }, + ], + }); + + const bundler: string = await prompt.run>( + select, + { + message: 'Select a bundler', + choices: [ + { + name: 'None', + value: 'base', + }, + { + name: 'Vite', + value: 'vite', + }, + { + name: 'webpack', + value: 'webpack', + }, + ], + }, + ); + + let language: string | undefined; + + if (bundler !== 'base') { + language = await prompt.run>( + select, + { + message: 'Select a programming language', + choices: [ + { + name: 'JavaScript', + value: undefined, + }, + { + name: 'TypeScript', + value: 'typescript', + }, + ], + }, + ); + } + + initOpts.packageManager = packageManager; + 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, + })); + }, + }, + ], + { concurrent: false }, + ); + + const initOpts: InitOptions = await tasks.run(); + await init(initOpts); + }); + +program + .command('import') + .description('Import an existing Electron project to use Electron Forge.') + .argument( + '[dir]', + 'Directory of the project to import. Defaults to the current directory.', + ) + .option( + '--skip-git', + 'Skip initializing a git repository in the imported project.', + ) + .action(async (dir, options) => { + const workingDir = resolveWorkingDir(dir); + await forgeImport({ + dir: workingDir, + interactive: true, + skipGit: Boolean(options.skipGit), + }); + }); + +program.parse(process.argv); diff --git a/packages/external/create-electron-app/src/import-core.ts b/packages/external/create-electron-app/src/import.ts similarity index 100% rename from packages/external/create-electron-app/src/import-core.ts rename to packages/external/create-electron-app/src/import.ts diff --git a/packages/external/create-electron-app/src/index.ts b/packages/external/create-electron-app/src/index.ts index f44db99539..0f7cfc7f9f 100644 --- a/packages/external/create-electron-app/src/index.ts +++ b/packages/external/create-electron-app/src/index.ts @@ -36,4 +36,4 @@ process.on('uncaughtException', (err) => { process.exit(1); }); -import './init'; +import './create-electron-app'; diff --git a/packages/external/create-electron-app/src/init.ts b/packages/external/create-electron-app/src/init.ts index 78c7b7371d..352c866462 100644 --- a/packages/external/create-electron-app/src/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -1,200 +1,278 @@ import fs from 'node:fs'; import path from 'node:path'; -import { confirm, select } from '@inquirer/prompts'; -import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'; +import { + DepType, + DepVersionRestriction, + installDependencies, + PMDetails, + resolvePackageManager, +} from '@electron-forge/core-utils'; +import { ForgeTemplate } from '@electron-forge/shared-types'; +import { spawn } from '@malept/cross-spawn-promise'; import chalk from 'chalk'; -import { program } from 'commander'; +import debug from 'debug'; import { Listr } from 'listr2'; +import semver from 'semver'; -import packageJSON from '../package.json'; +import { findTemplate } from './init-scripts/find-template'; +import { initDirectory } from './init-scripts/init-directory'; +import { initGit } from './init-scripts/init-git'; +import { initLink } from './init-scripts/init-link'; +import { initNPM } from './init-scripts/init-npm'; -import { init, InitOptions } from './core'; +const d = debug('electron-forge:init'); -// eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` -import type { Prompt } from '@inquirer/type'; +export interface InitOptions { + /** + * The path to the app to be initialized + */ + dir?: string; + /** + * Whether to use sensible defaults or prompt the user visually + */ + interactive?: boolean; + /** + * Whether to copy template CI files + */ + copyCIFiles?: boolean; + /** + * Whether to overwrite an existing directory + */ + force?: boolean; + /** + * The custom template to use. If left empty, the default template is used + */ + template?: string; + /** + * 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; + /** + * Force a package manager to use (npm|yarn|pnpm). + */ + packageManager?: string; +} -/** - * Resolves the directory in which to use a CLI command. - * @param dir - The directory specified by the user (can be relative or absolute) - * @param checkExisting - Checks if the directory exists. If true and directory is non-existent, it will fall back to the current working directory - * @returns - */ -export function resolveWorkingDir(dir: string, checkExisting = true): string { - if (!dir) { - return process.cwd(); +async function validateTemplate( + template: string, + templateModule: ForgeTemplate, +): Promise { + if (!templateModule.requiredForgeVersion) { + throw new Error( + `Cannot use a template (${template}) with this version of Electron Forge, as it does not specify its required Forge version.`, + ); } - const resolved = path.isAbsolute(dir) - ? dir - : path.resolve(process.cwd(), dir); + const dir = path.join(__dirname, '..'); + const raw = await fs.promises.readFile( + path.join(dir, 'package.json'), + 'utf-8', + ); + const packageJSON = JSON.parse(raw); - if (checkExisting && !fs.existsSync(resolved)) { - return process.cwd(); - } else { - return resolved; + const forgeVersion = packageJSON.version; + if (!semver.satisfies(forgeVersion, templateModule.requiredForgeVersion)) { + throw new Error( + `Template (${template}) is not compatible with this version of Electron Forge (${forgeVersion}), it requires ${templateModule.requiredForgeVersion}`, + ); } } -program - .version(packageJSON.version, '-V, --version', 'Output the current version.') - .helpOption('-h, --help', 'Output usage information.') - .argument( - '[dir]', - 'Directory to initialize the project in. Defaults to the current directory.', - ) - .option('-t, --template [name]', 'Name of the Forge template to use.') - .option('-c, --copy-ci-files', 'Whether to copy the templated CI files.') - .option('-f, --force', 'Whether to overwrite an existing directory.') - .option( - '--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` tags.', - ) - .option( - '--package-manager [name]', - 'Set a specific package manager to use for your Forge project. Supported package managers are `npm`, `pnpm`, and `yarn`. You can also specify an exact version to use (e.g. `yarn@1.22.22`).', - ) - .action(async (dir) => { - const options = program.opts(); - const tasks = new Listr( - [ - { - task: async (initOpts): Promise => { - initOpts.interactive = true; - initOpts.template = options.template ?? 'base'; - initOpts.copyCIFiles = Boolean(options.copyCiFiles); - initOpts.force = Boolean(options.force); - initOpts.skipGit = Boolean(options.skipGit); - initOpts.dir = resolveWorkingDir(dir, false); - initOpts.electronVersion = options.electronVersion ?? 'latest'; - initOpts.packageManager = options.packageManager ?? 'npm@latest'; - }, - }, - { - task: async (initOpts, task): Promise => { - if ( - Object.keys(options).length > 0 || - process.env.CI || - !process.stdout.isTTY - ) { - return; - } +export async function init({ + dir = process.cwd(), + interactive = false, + copyCIFiles = false, + force = false, + template = 'base', + skipGit = false, + electronVersion = 'latest', + packageManager, +}: InitOptions): Promise { + d(`Initializing in: ${dir}`); - const prompt = task.prompt(ListrInquirerPromptAdapter); - - if ( - typeof initOpts.dir === 'string' && - fs.existsSync(initOpts.dir) && - (await fs.promises.readdir(initOpts.dir)).length > 0 - ) { - const confirmResult = await prompt.run(confirm, { - message: `${chalk.cyan(initOpts.dir)} is not empty. Would you like to continue and overwrite existing files?`, - default: false, - }); + const runner = new Listr<{ + templateModule: ForgeTemplate; + pm: PMDetails; + parsedElectronVersion: string; + }>( + [ + { + title: `Resolving package manager`, + task: async (ctx, task) => { + ctx.pm = await resolvePackageManager(packageManager); + task.title = `Resolved package manager: ${chalk.cyan(`${ctx.pm.executable}@${ctx.pm.version}`)}`; + }, + }, + { + title: `Resolving template: ${chalk.cyan(template)}`, + task: async (ctx, task) => { + const tmpl = await findTemplate(template); + ctx.templateModule = tmpl.template; + task.output = `Using ${chalk.green(tmpl.name)}`; + }, + 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)}`; + ctx.parsedElectronVersion = electronVersion; + } else { + // semver.clean allows us to accept `v` versions and trims whitespace + const maybeVersion = semver.clean(electronVersion); - if (confirmResult) { - initOpts.force = true; - } else { - task.output = 'Directory is not empty. Exiting.'; - process.exit(0); - } + 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'.`, + ); } - - const packageManager: string = await prompt.run< - Prompt - >(select, { - message: 'Select a package manager', - choices: [ - { name: 'npm', value: 'npm@latest' }, - { name: 'pnpm', value: 'pnpm@latest' }, - { name: 'Yarn (Berry)', value: 'yarn@latest' }, - { name: 'Yarn (Classic)', value: 'yarn@1' }, - ], + } + }, + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Initializing directory', + task: async (_, task) => { + await initDirectory(dir, task, force); + }, + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Initializing git repository', + enabled: !skipGit, + task: async () => { + await initGit(dir); + }, + }, + { + title: 'Preparing template', + task: async ({ templateModule }) => { + await validateTemplate(template, templateModule); + }, + }, + { + title: `Initializing template`, + task: async ({ templateModule }, task) => { + if (typeof templateModule.initializeTemplate === 'function') { + const tasks = await templateModule.initializeTemplate(dir, { + copyCIFiles, + force, }); - - const bundler: string = await prompt.run>( - select, + if (tasks) { + return task.newListr(tasks, { concurrent: false }); + } + } + }, + }, + { + title: `Setting package manager with Corepack`, + // pm.executable needs to be optional here because the code gets evaluated twice (on init and on execution) + // @see https://listr2.kilic.dev/task/enable.html + enabled: ({ pm }) => pm?.executable !== 'npm', + task: async ({ pm }, task) => { + const pmString = `${pm.executable}@${pm.version}`; + try { + await spawn('corepack', ['use', pmString], { + cwd: dir, + }); + task.title = `Set ${chalk.cyan(pmString)} via Corepack`; + } catch (e) { + d('corepack failed to run with error', e); + task.title = `Forge was unable to set ${chalk.cyan(pmString)} via Corepack and will fall back to ${chalk.cyan('npm')}. If you are using Node.js >= 25, you will need to install corepack via ${chalk.green('npm install -g corepack')}. Otherwise, you may need to enable Corepack shims via ${chalk.green('corepack enable')}.`; + } + }, + }, + { + title: 'Installing template dependencies', + task: async ({ templateModule }, task) => { + return task.newListr( + [ { - message: 'Select a bundler', - choices: [ - { - name: 'None', - value: 'base', - }, - { - name: 'Vite', - value: 'vite', - }, - { - name: 'webpack', - value: 'webpack', - }, - ], + title: 'Installing production dependencies', + task: async ({ pm }, task) => { + d('installing dependencies'); + if (templateModule.dependencies?.length) { + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.dependencies.join(' ')}`; + } + return await installDependencies( + pm, + dir, + templateModule.dependencies || [], + DepType.PROD, + DepVersionRestriction.RANGE, + ); + }, + exitOnError: false, }, - ); - - let language: string | undefined; - - if (bundler !== 'base') { - language = await prompt.run>( - select, - { - message: 'Select a programming language', - choices: [ + { + title: 'Installing development dependencies', + task: async ({ pm }, task) => { + d('installing devDependencies'); + if (templateModule.devDependencies?.length) { + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.devDependencies.join(' ')}`; + } + await installDependencies( + pm, + dir, + templateModule.devDependencies || [], + DepType.DEV, + ); + }, + exitOnError: false, + }, + { + title: 'Finalizing dependencies', + task: async (ctx, task) => { + return task.newListr([ { - name: 'JavaScript', - value: undefined, + title: 'Installing common dependencies', + task: async ({ pm }, task) => { + await initNPM(pm, dir, ctx.parsedElectronVersion, task); + }, + exitOnError: false, }, { - name: 'TypeScript', - value: 'typescript', + title: 'Linking Forge dependencies to local build', + enabled: !!process.env.LINK_FORGE_DEPENDENCIES_ON_INIT, + task: async ({ pm }, task) => { + await initLink(pm, dir, task); + }, + exitOnError: true, }, - ], + ]); }, - ); - } - - initOpts.packageManager = packageManager; - 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, - })); - }, + ], + { + concurrent: false, + }, + ); }, - ], - { concurrent: false }, - ); + }, + ], + { + concurrent: false, + silentRendererCondition: !interactive, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }, + ); - const initOpts: InitOptions = await tasks.run(); - await init(initOpts); - }); - -program.parse(process.argv); + await runner.run(); +} From 08d80c385f1c4d8fed6d5e6d58f7199f7acf26ab Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 27 Feb 2026 23:32:22 -0800 Subject: [PATCH 11/27] clean up --- packages/api/cli/package.json | 2 -- packages/api/core/spec/slow/init.slow.verdaccio.spec.ts | 2 +- .../spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts | 2 +- .../spec/WebpackTypeScript.slow.verdaccio.spec.ts | 2 +- yarn.lock | 2 -- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/api/cli/package.json b/packages/api/cli/package.json index c4770cc625..e369c3a96c 100644 --- a/packages/api/cli/package.json +++ b/packages/api/cli/package.json @@ -19,8 +19,6 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@electron/get": "^3.0.0", - "@inquirer/prompts": "^6.0.1", - "@listr2/prompt-adapter-inquirer": "^2.0.22", "chalk": "^4.0.0", "commander": "^11.1.0", "debug": "^4.3.1", diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts index 00ddc4fe67..58da72c4ea 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts @@ -8,7 +8,7 @@ import { import semver from 'semver'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { init } from '../../../../external/create-electron-app/src/core'; +import { init } from '../../../../external/create-electron-app/src/init'; describe('init', () => { let dir: string; diff --git a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts index 0a24e913f6..7af5ebc62f 100644 --- a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts +++ b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts @@ -12,7 +12,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; // eslint-disable-next-line n/no-missing-import import { api } from '../../../api/core/dist/api'; -import { init } from '../../../external/create-electron-app/src/core'; +import { init } from '../../../external/create-electron-app/src/init'; describe('ViteTypeScriptTemplate', () => { let dir: string; diff --git a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts index 0af895d128..bf22e26a99 100644 --- a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts +++ b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts @@ -11,7 +11,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; // eslint-disable-next-line n/no-missing-import import { api } from '../../../api/core/dist/api'; -import { init } from '../../../external/create-electron-app/src/core'; +import { init } from '../../../external/create-electron-app/src/init'; describe('WebpackTypeScriptTemplate', () => { let dir: string; diff --git a/yarn.lock b/yarn.lock index f8482f356e..fc3ae6230d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,8 +813,6 @@ __metadata: "@electron-forge/core-utils": "workspace:*" "@electron-forge/shared-types": "workspace:*" "@electron/get": "npm:^3.0.0" - "@inquirer/prompts": "npm:^6.0.1" - "@listr2/prompt-adapter-inquirer": "npm:^2.0.22" "@malept/cross-spawn-promise": "npm:^2.0.0" chalk: "npm:^4.0.0" commander: "npm:^11.1.0" From 47b372cf6d5cf0b4338c06a46edab14ebb5b0481 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 27 Feb 2026 23:41:03 -0800 Subject: [PATCH 12/27] yea aight --- .../create-electron-app/spec/slow/import.slow.verdaccio.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts b/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts index 2480a8044b..8a429c68c6 100644 --- a/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts +++ b/packages/external/create-electron-app/spec/slow/import.slow.verdaccio.spec.ts @@ -9,7 +9,7 @@ import { } from '@electron-forge/test-utils'; import { beforeEach, describe, expect, it } from 'vitest'; -import { forgeImport } from '../../src/import-core'; +import { forgeImport } from '../../src/import'; describe('import', () => { let dir: string; From eae98db3875c436595d6593673c4cd86272a4b4d Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 5 Mar 2026 16:06:52 -0800 Subject: [PATCH 13/27] move tests and fixtures --- .../create-electron-app}/spec/fixture/custom_init/index.cjs | 0 .../spec/fixture/custom_init/package-lock.json | 0 .../create-electron-app}/spec/fixture/custom_init/package.json | 0 .../create-electron-app}/spec/fixture/custom_init/tmpl/_bar | 0 .../spec/fixture/custom_init/tmpl/src/foo.js | 0 .../spec/fixture/template-sans-forge-version/index.js | 0 .../spec/fixture/template-sans-forge-version/package.json | 0 .../spec/fixture/template-stale-forge-version/index.cjs | 0 .../spec/fixture/template-stale-forge-version/package-lock.json | 0 .../spec/fixture/template-stale-forge-version/package.json | 0 .../create-electron-app}/spec/slow/init.slow.verdaccio.spec.ts | 2 +- 11 files changed, 1 insertion(+), 1 deletion(-) rename packages/{api/core => external/create-electron-app}/spec/fixture/custom_init/index.cjs (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/custom_init/package-lock.json (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/custom_init/package.json (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/custom_init/tmpl/_bar (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/custom_init/tmpl/src/foo.js (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/template-sans-forge-version/index.js (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/template-sans-forge-version/package.json (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/template-stale-forge-version/index.cjs (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/template-stale-forge-version/package-lock.json (100%) rename packages/{api/core => external/create-electron-app}/spec/fixture/template-stale-forge-version/package.json (100%) rename packages/{api/core => external/create-electron-app}/spec/slow/init.slow.verdaccio.spec.ts (99%) diff --git a/packages/api/core/spec/fixture/custom_init/index.cjs b/packages/external/create-electron-app/spec/fixture/custom_init/index.cjs similarity index 100% rename from packages/api/core/spec/fixture/custom_init/index.cjs rename to packages/external/create-electron-app/spec/fixture/custom_init/index.cjs diff --git a/packages/api/core/spec/fixture/custom_init/package-lock.json b/packages/external/create-electron-app/spec/fixture/custom_init/package-lock.json similarity index 100% rename from packages/api/core/spec/fixture/custom_init/package-lock.json rename to packages/external/create-electron-app/spec/fixture/custom_init/package-lock.json diff --git a/packages/api/core/spec/fixture/custom_init/package.json b/packages/external/create-electron-app/spec/fixture/custom_init/package.json similarity index 100% rename from packages/api/core/spec/fixture/custom_init/package.json rename to packages/external/create-electron-app/spec/fixture/custom_init/package.json diff --git a/packages/api/core/spec/fixture/custom_init/tmpl/_bar b/packages/external/create-electron-app/spec/fixture/custom_init/tmpl/_bar similarity index 100% rename from packages/api/core/spec/fixture/custom_init/tmpl/_bar rename to packages/external/create-electron-app/spec/fixture/custom_init/tmpl/_bar diff --git a/packages/api/core/spec/fixture/custom_init/tmpl/src/foo.js b/packages/external/create-electron-app/spec/fixture/custom_init/tmpl/src/foo.js similarity index 100% rename from packages/api/core/spec/fixture/custom_init/tmpl/src/foo.js rename to packages/external/create-electron-app/spec/fixture/custom_init/tmpl/src/foo.js diff --git a/packages/api/core/spec/fixture/template-sans-forge-version/index.js b/packages/external/create-electron-app/spec/fixture/template-sans-forge-version/index.js similarity index 100% rename from packages/api/core/spec/fixture/template-sans-forge-version/index.js rename to packages/external/create-electron-app/spec/fixture/template-sans-forge-version/index.js diff --git a/packages/api/core/spec/fixture/template-sans-forge-version/package.json b/packages/external/create-electron-app/spec/fixture/template-sans-forge-version/package.json similarity index 100% rename from packages/api/core/spec/fixture/template-sans-forge-version/package.json rename to packages/external/create-electron-app/spec/fixture/template-sans-forge-version/package.json diff --git a/packages/api/core/spec/fixture/template-stale-forge-version/index.cjs b/packages/external/create-electron-app/spec/fixture/template-stale-forge-version/index.cjs similarity index 100% rename from packages/api/core/spec/fixture/template-stale-forge-version/index.cjs rename to packages/external/create-electron-app/spec/fixture/template-stale-forge-version/index.cjs diff --git a/packages/api/core/spec/fixture/template-stale-forge-version/package-lock.json b/packages/external/create-electron-app/spec/fixture/template-stale-forge-version/package-lock.json similarity index 100% rename from packages/api/core/spec/fixture/template-stale-forge-version/package-lock.json rename to packages/external/create-electron-app/spec/fixture/template-stale-forge-version/package-lock.json diff --git a/packages/api/core/spec/fixture/template-stale-forge-version/package.json b/packages/external/create-electron-app/spec/fixture/template-stale-forge-version/package.json similarity index 100% rename from packages/api/core/spec/fixture/template-stale-forge-version/package.json rename to packages/external/create-electron-app/spec/fixture/template-stale-forge-version/package.json diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/external/create-electron-app/spec/slow/init.slow.verdaccio.spec.ts similarity index 99% rename from packages/api/core/spec/slow/init.slow.verdaccio.spec.ts rename to packages/external/create-electron-app/spec/slow/init.slow.verdaccio.spec.ts index a62a3a81c4..ed3859cadc 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/external/create-electron-app/spec/slow/init.slow.verdaccio.spec.ts @@ -8,7 +8,7 @@ import { import semver from 'semver'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { init } from '../../../../external/create-electron-app/src/init'; +import { init } from '../../src/init'; describe('init', () => { let dir: string; From e3a5bc8f91e9defc0f9a75d99f53103ffacffa9c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 5 Mar 2026 16:19:15 -0800 Subject: [PATCH 14/27] delete dynamic-import --- .eslintignore | 1 - .../core-utils/helper/dynamic-import.d.ts | 3 --- .../utils/core-utils/helper/dynamic-import.js | 23 ------------------- 3 files changed, 27 deletions(-) delete mode 100644 packages/utils/core-utils/helper/dynamic-import.d.ts delete mode 100644 packages/utils/core-utils/helper/dynamic-import.js diff --git a/.eslintignore b/.eslintignore index ac8c1e5dca..efb66f5bb4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,5 +6,4 @@ packages/*/*/doc packages/*/*/index.ts packages/**/bad.js tmpl -packages/api/core/helper/dynamic-import.js packages/api/core/spec/fixture/api-tester/ diff --git a/packages/utils/core-utils/helper/dynamic-import.d.ts b/packages/utils/core-utils/helper/dynamic-import.d.ts deleted file mode 100644 index 4d25e1299a..0000000000 --- a/packages/utils/core-utils/helper/dynamic-import.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare function dynamicImport(path: string): Promise; -/** Like {@link dynamicImport()}, except it tries out {@link require()} first. */ -export declare function dynamicImportMaybe(path: string): Promise; diff --git a/packages/utils/core-utils/helper/dynamic-import.js b/packages/utils/core-utils/helper/dynamic-import.js deleted file mode 100644 index 728f341307..0000000000 --- a/packages/utils/core-utils/helper/dynamic-import.js +++ /dev/null @@ -1,23 +0,0 @@ -const fs = require('fs'); -const url = require('url'); - -exports.dynamicImport = async function dynamicImport(path) { - try { - return await import(fs.existsSync(path) ? url.pathToFileURL(path) : path); - } catch (error) { - return Promise.reject(error); - } -}; - -exports.dynamicImportMaybe = async function dynamicImportMaybe(path) { - try { - return require(path); - } catch (e1) { - try { - return await exports.dynamicImport(path); - } catch (e2) { - e1.message = '\n1. ' + e1.message + '\n2. ' + e2.message; - throw e1; - } - } -}; From 3f7b9985a0b837688c7f60431e8c174d1b4a89c3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 5 Mar 2026 16:29:29 -0800 Subject: [PATCH 15/27] restore importSearch --- packages/api/core/src/util/import-search.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/api/core/src/util/import-search.ts b/packages/api/core/src/util/import-search.ts index b5b42f668c..c70963f6c4 100644 --- a/packages/api/core/src/util/import-search.ts +++ b/packages/api/core/src/util/import-search.ts @@ -84,6 +84,15 @@ export type PossibleModule = { default?: T; } & T; +/** + * Used throughout `@electron-forge` to dynamically load makers, publishers, + * plugins, and lifecycle hooks by package name. Only accepts default exports. + * + * @param relativeTo - Directory to resolve relative paths against (typically the project root). + * @param paths - Module specifiers to attempt (e.g. `['@electron-forge/maker-zip']`). + * @returns The module's default export, or `null` if the module was not found + * or has no default export. + */ export async function importSearch( relativeTo: string, paths: string[], @@ -91,5 +100,5 @@ export async function importSearch( const result = await importSearchRaw>(relativeTo, paths); return typeof result === 'object' && result && result.default ? result.default - : (result as T | null); + : null; } From 363baac154f73ec9ff022a23d6c296ede32ba883 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 5 Mar 2026 23:21:36 -0800 Subject: [PATCH 16/27] move resolve-working-dir to core-utils --- packages/api/cli/src/electron-forge-make.ts | 3 +-- .../api/cli/src/electron-forge-package.ts | 3 +-- .../api/cli/src/electron-forge-publish.ts | 2 +- packages/api/cli/src/electron-forge-start.ts | 3 +-- .../api/cli/src/util/resolve-working-dir.ts | 25 ------------------- .../src/create-electron-app.ts | 2 +- .../spec}/resolve-working-dir.spec.ts | 2 +- packages/utils/core-utils/src/index.ts | 1 + .../core-utils/src}/resolve-working-dir.ts | 0 9 files changed, 7 insertions(+), 34 deletions(-) delete mode 100644 packages/api/cli/src/util/resolve-working-dir.ts rename packages/{api/cli/spec/util => utils/core-utils/spec}/resolve-working-dir.spec.ts (95%) rename packages/{external/create-electron-app/src/util => utils/core-utils/src}/resolve-working-dir.ts (100%) diff --git a/packages/api/cli/src/electron-forge-make.ts b/packages/api/cli/src/electron-forge-make.ts index 41449ffe55..1c3cf55923 100644 --- a/packages/api/cli/src/electron-forge-make.ts +++ b/packages/api/cli/src/electron-forge-make.ts @@ -1,13 +1,12 @@ import { initializeProxy } from '@electron/get'; import { api, MakeOptions } from '@electron-forge/core'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; import chalk from 'chalk'; import { program } from 'commander'; import './util/terminate.js'; import packageJSON from '../package.json' with { type: 'json' }; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; - export async function getMakeOptions(): Promise { let workingDir: string; program diff --git a/packages/api/cli/src/electron-forge-package.ts b/packages/api/cli/src/electron-forge-package.ts index 5e28d8b0f2..5451f0baf2 100644 --- a/packages/api/cli/src/electron-forge-package.ts +++ b/packages/api/cli/src/electron-forge-package.ts @@ -1,12 +1,11 @@ import { initializeProxy } from '@electron/get'; import { api, PackageOptions } from '@electron-forge/core'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; import { program } from 'commander'; import './util/terminate.js'; import packageJSON from '../package.json' with { type: 'json' }; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; - program .version(packageJSON.version, '-V, --version', 'Output the current version') .helpOption('-h, --help', 'Output usage information') diff --git a/packages/api/cli/src/electron-forge-publish.ts b/packages/api/cli/src/electron-forge-publish.ts index d5018ecfe1..f8db5c182f 100644 --- a/packages/api/cli/src/electron-forge-publish.ts +++ b/packages/api/cli/src/electron-forge-publish.ts @@ -1,5 +1,6 @@ import { initializeProxy } from '@electron/get'; import { api, PublishOptions } from '@electron-forge/core'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; import chalk from 'chalk'; import { program } from 'commander'; @@ -7,7 +8,6 @@ import './util/terminate.js'; import packageJSON from '../package.json' with { type: 'json' }; import { getMakeOptions } from './electron-forge-make.js'; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; program .version(packageJSON.version, '-V, --version', 'Output the current version.') diff --git a/packages/api/cli/src/electron-forge-start.ts b/packages/api/cli/src/electron-forge-start.ts index fbecc1b4c8..a81ec64ec2 100644 --- a/packages/api/cli/src/electron-forge-start.ts +++ b/packages/api/cli/src/electron-forge-start.ts @@ -1,12 +1,11 @@ import { api, StartOptions } from '@electron-forge/core'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; import { ElectronProcess } from '@electron-forge/shared-types'; import { Option, program } from 'commander'; import './util/terminate.js'; import packageJSON from '../package.json' with { type: 'json' }; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; - (async () => { let commandArgs = process.argv; let appArgs; diff --git a/packages/api/cli/src/util/resolve-working-dir.ts b/packages/api/cli/src/util/resolve-working-dir.ts deleted file mode 100644 index a622543d15..0000000000 --- a/packages/api/cli/src/util/resolve-working-dir.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from 'node:path'; - -import fs from 'fs-extra'; - -/** - * Resolves the directory in which to use a CLI command. - * @param dir - The directory specified by the user (can be relative or absolute) - * @param checkExisting - Checks if the directory exists. If true and directory is non-existent, it will fall back to the current working directory - * @returns - */ -export function resolveWorkingDir(dir: string, checkExisting = true): string { - if (!dir) { - return process.cwd(); - } - - const resolved = path.isAbsolute(dir) - ? dir - : path.resolve(process.cwd(), dir); - - if (checkExisting && !fs.existsSync(resolved)) { - return process.cwd(); - } else { - return resolved; - } -} diff --git a/packages/external/create-electron-app/src/create-electron-app.ts b/packages/external/create-electron-app/src/create-electron-app.ts index 28f269b03b..851e26cf46 100644 --- a/packages/external/create-electron-app/src/create-electron-app.ts +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; import { confirm, select } from '@inquirer/prompts'; import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer'; import chalk from 'chalk'; @@ -10,7 +11,6 @@ import packageJSON from '../package.json' with { type: 'json' }; import { forgeImport } from './import.js'; import { init, InitOptions } from './init.js'; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; // eslint-disable-next-line n/no-extraneous-import -- we get this from `@inquirer/prompts` import type { Prompt } from '@inquirer/type'; diff --git a/packages/api/cli/spec/util/resolve-working-dir.spec.ts b/packages/utils/core-utils/spec/resolve-working-dir.spec.ts similarity index 95% rename from packages/api/cli/spec/util/resolve-working-dir.spec.ts rename to packages/utils/core-utils/spec/resolve-working-dir.spec.ts index 88832bbe3e..e018c45db5 100644 --- a/packages/api/cli/spec/util/resolve-working-dir.spec.ts +++ b/packages/utils/core-utils/spec/resolve-working-dir.spec.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { resolveWorkingDir } from '../../src/util/resolve-working-dir'; +import { resolveWorkingDir } from '../src/resolve-working-dir'; describe('resolveWorkingDir', () => { it('resolves relative paths according to the current working directory', () => { diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index d2ce0b395d..36865466bd 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -3,3 +3,4 @@ export * from './electron-version.js'; export * from './package-manager.js'; export * from './author-name.js'; export * from './install-dependencies.js'; +export * from './resolve-working-dir.js'; diff --git a/packages/external/create-electron-app/src/util/resolve-working-dir.ts b/packages/utils/core-utils/src/resolve-working-dir.ts similarity index 100% rename from packages/external/create-electron-app/src/util/resolve-working-dir.ts rename to packages/utils/core-utils/src/resolve-working-dir.ts From ef5539dc7651b0d6cf4e6b306cceef42b41b3a76 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 5 Mar 2026 23:21:51 -0800 Subject: [PATCH 17/27] revert --- packages/api/core/src/util/import-search.ts | 4 ++-- .../external/create-electron-app/src/import.ts | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/api/core/src/util/import-search.ts b/packages/api/core/src/util/import-search.ts index c70963f6c4..ce782f16ff 100644 --- a/packages/api/core/src/util/import-search.ts +++ b/packages/api/core/src/util/import-search.ts @@ -33,18 +33,18 @@ async function importSearchRaw( ): Promise { // Attempt to locally short-circuit if we're running from a checkout of forge if ( - import.meta.dirname.includes('forge/packages/') && + import.meta.dirname.includes('forge/packages/api/core/') && paths.length === 1 && paths[0].startsWith('@electron-forge/') ) { const [moduleType, moduleName] = paths[0].split('/')[1].split('-'); try { - // From packages/utils/core-utils/dist (or src), resolve up to packages/ const localPath = path.resolve( import.meta.dirname, '..', '..', '..', + '..', moduleType, moduleName, ); diff --git a/packages/external/create-electron-app/src/import.ts b/packages/external/create-electron-app/src/import.ts index c6137dd87e..53fd1fd0eb 100644 --- a/packages/external/create-electron-app/src/import.ts +++ b/packages/external/create-electron-app/src/import.ts @@ -123,7 +123,7 @@ export async function forgeImport({ persistentOutput: true, bottomBar: Infinity, }, - task: async (_ctx: any, task: any) => { + task: async (_ctx, task) => { const calculatedOutDir = outDir || 'out'; const importDeps = ([] as string[]).concat(deps); @@ -240,14 +240,14 @@ export async function forgeImport({ [ { title: `Resolving package manager`, - task: async (ctx: { pm: PMDetails }, task: any) => { + task: async (ctx, task) => { ctx.pm = await resolvePackageManager(); task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; }, }, { title: 'Configuring Yarn (if applicable)', - task: async ({ pm }: { pm: PMDetails }) => { + task: async ({ pm }) => { // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config // lets ensure that nodeLinker is set to node-modules if (pm.executable === 'yarn') { @@ -264,7 +264,7 @@ export async function forgeImport({ }, { title: 'Installing dependencies', - task: async ({ pm }: { pm: PMDetails }, task: any) => { + task: async ({ pm }, task) => { await writeChanges(); d('deleting old dependencies forcefully'); @@ -315,9 +315,8 @@ export async function forgeImport({ d( 'detected existing Forge config in package.json, merging with base template Forge config', ); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const templateConfig = require( - path.resolve(baseTemplate.templateDir, 'forge.config.js'), + const templateConfig = await import( + path.resolve(baseTemplate.templateDir, 'forge.config.js') ); packageJSON = await readRawPackageJson(dir); merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object @@ -349,7 +348,7 @@ export async function forgeImport({ }, }, ], - listrOptions, + { concurrent: listrOptions.concurrent }, ); }, }, @@ -359,7 +358,7 @@ export async function forgeImport({ persistentOutput: true, bottomBar: Infinity, }, - task: (_: any, task: any) => { + task: (_ctx, task) => { task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. Thanks for using ${chalk.green('Electron Forge')}!`; From 101a2dcc7cea240e87183ca4d17ff51c407acfc2 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 10:23:35 -0800 Subject: [PATCH 18/27] standardize lodash-es --- packages/api/core/package.json | 1 - packages/external/create-electron-app/package.json | 2 +- packages/external/create-electron-app/src/import.ts | 2 +- yarn.lock | 3 +-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/api/core/package.json b/packages/api/core/package.json index f0eaaea16d..2a9989f28e 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -48,7 +48,6 @@ "got": "^14.0.0", "jiti": "^2.4.2", "listr2": "^7.0.2", - "lodash-es": "^4.17.21", "log-symbols": "^4.0.0", "semver": "^7.2.1", "source-map-support": "^0.5.13", diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index c1446f001c..807ba7c93f 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -24,7 +24,7 @@ "debug": "^4.3.1", "fs-extra": "^10.0.0", "listr2": "^7.0.2", - "lodash": "^4.17.20", + "lodash-es": "^4.17.21", "log-symbols": "^4.0.0", "semver": "^7.2.1" }, diff --git a/packages/external/create-electron-app/src/import.ts b/packages/external/create-electron-app/src/import.ts index 53fd1fd0eb..fea3c332dc 100644 --- a/packages/external/create-electron-app/src/import.ts +++ b/packages/external/create-electron-app/src/import.ts @@ -14,7 +14,7 @@ import chalk from 'chalk'; import debug from 'debug'; import fs from 'fs-extra'; import { Listr } from 'listr2'; -import { merge } from 'lodash'; +import { merge } from 'lodash-es'; import { initGit } from './init-scripts/init-git.js'; import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm.js'; diff --git a/yarn.lock b/yarn.lock index 4126686434..29c3d8f0d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -886,7 +886,6 @@ __metadata: got: "npm:^14.0.0" jiti: "npm:^2.4.2" listr2: "npm:^7.0.2" - lodash-es: "npm:^4.17.21" log-symbols: "npm:^4.0.0" semver: "npm:^7.2.1" source-map-support: "npm:^0.5.13" @@ -7983,7 +7982,7 @@ __metadata: debug: "npm:^4.3.1" fs-extra: "npm:^10.0.0" listr2: "npm:^7.0.2" - lodash: "npm:^4.17.20" + lodash-es: "npm:^4.17.21" log-symbols: "npm:^4.0.0" semver: "npm:^7.2.1" bin: From 41fe4e5b472ff6d44c9a208adc8f62aaf9eb2b86 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 12:24:16 -0800 Subject: [PATCH 19/27] restore `resolveWorkingDir` fallback `false` flag --- .../external/create-electron-app/src/create-electron-app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external/create-electron-app/src/create-electron-app.ts b/packages/external/create-electron-app/src/create-electron-app.ts index 851e26cf46..b3cb52cc36 100644 --- a/packages/external/create-electron-app/src/create-electron-app.ts +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -192,7 +192,7 @@ program 'Skip initializing a git repository in the imported project.', ) .action(async (dir, options) => { - const workingDir = resolveWorkingDir(dir); + const workingDir = resolveWorkingDir(dir, false); await forgeImport({ dir: workingDir, interactive: true, From a7e6f17468facfdcb597a63e3f1518a036e47451 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 13:20:03 -0800 Subject: [PATCH 20/27] chore: fix up devDeps --- packages/external/create-electron-app/package.json | 3 +++ yarn.lock | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 807ba7c93f..16f129ad3d 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -28,6 +28,9 @@ "log-symbols": "^4.0.0", "semver": "^7.2.1" }, + "devDependencies": { + "@electron-forge/core": "workspace:*" + }, "bin": "dist/index.js", "files": [ "dist", diff --git a/yarn.lock b/yarn.lock index 29c3d8f0d4..627fa67618 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7967,6 +7967,7 @@ __metadata: version: 0.0.0-use.local resolution: "create-electron-app@workspace:packages/external/create-electron-app" dependencies: + "@electron-forge/core": "workspace:*" "@electron-forge/core-utils": "workspace:*" "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" From d5a5a06c249fade780f3d9554903f62ea1d530a0 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 13:20:23 -0800 Subject: [PATCH 21/27] fix: restore deleted findTemplate tests --- .../fast/init-scripts/find-template.spec.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts diff --git a/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts new file mode 100644 index 0000000000..c4412bb4d5 --- /dev/null +++ b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; + +import { findTemplate } from '../../../src/init-scripts/find-template'; + +describe('findTemplate', () => { + /** + * Note: this test suite does not mock `require.resolve`. Instead, it uses + * fixture dependencies defined in this module's package.json file to + * actually resolve a local template. + * + * If you modify the fixtures, you may need to re-run `yarn install` in order + * for the fixtures to be installed in your local `node_modules`. + */ + describe('local modules', () => { + it('should find an @electron-forge/template based on partial name', async () => { + await expect(findTemplate('fixture')).resolves.toEqual( + expect.objectContaining({ name: '@electron-forge/template-fixture' }), + ); + }); + + it('should find an @electron-forge/template based on full name', async () => { + await expect( + findTemplate('@electron-forge/template-fixture'), + ).resolves.toEqual( + expect.objectContaining({ name: '@electron-forge/template-fixture' }), + ); + }); + it('should find an electron-forge-template based on partial name', async () => { + await expect(findTemplate('fixture-two')).resolves.toEqual( + expect.objectContaining({ + name: 'electron-forge-template-fixture-two', + }), + ); + }); + it('should find an @electron-forge-template based on full name', async () => { + await expect( + findTemplate('electron-forge-template-fixture-two'), + ).resolves.toEqual( + expect.objectContaining({ + name: 'electron-forge-template-fixture-two', + }), + ); + }); + }); + + it('should error if there are no valid templates', async () => { + await expect(findTemplate('non-existent-template')).rejects.toThrowError( + 'Failed to locate custom template: "non-existent-template".', + ); + }); +}); From 2f16d468833792739f66372ec7e0ce4df6a8df23 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 13:29:31 -0800 Subject: [PATCH 22/27] de-duplicate `PossibleModule` type --- packages/api/core/src/util/import-search.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/api/core/src/util/import-search.ts b/packages/api/core/src/util/import-search.ts index ce782f16ff..cad0c1afcd 100644 --- a/packages/api/core/src/util/import-search.ts +++ b/packages/api/core/src/util/import-search.ts @@ -1,5 +1,6 @@ import path from 'node:path'; +import { PossibleModule } from '@electron-forge/shared-types'; import debug from 'debug'; const d = debug('electron-forge:import-search'); @@ -79,11 +80,6 @@ async function importSearchRaw( return null; } -/** A module namespace that may or may not have a default export. */ -export type PossibleModule = { - default?: T; -} & T; - /** * Used throughout `@electron-forge` to dynamically load makers, publishers, * plugins, and lifecycle hooks by package name. Only accepts default exports. From ee225be2dddd0a439698e26af9d3c6cebaa06ba1 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 13:30:18 -0800 Subject: [PATCH 23/27] add tracer back to import --- .../external/create-electron-app/package.json | 1 + .../create-electron-app/src/import.ts | 572 +++++++++--------- 2 files changed, 301 insertions(+), 272 deletions(-) diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 16f129ad3d..90602fb1ed 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -12,6 +12,7 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@electron-forge/template-base": "workspace:*", + "@electron-forge/tracer": "workspace:*", "@electron-forge/template-vite": "workspace:*", "@electron-forge/template-vite-typescript": "workspace:*", "@electron-forge/template-webpack": "workspace:*", diff --git a/packages/external/create-electron-app/src/import.ts b/packages/external/create-electron-app/src/import.ts index fea3c332dc..89ecc35f59 100644 --- a/packages/external/create-electron-app/src/import.ts +++ b/packages/external/create-electron-app/src/import.ts @@ -8,8 +8,12 @@ import { resolvePackageManager, updateElectronDependency, } from '@electron-forge/core-utils'; -import { ForgeListrOptions } from '@electron-forge/shared-types'; +import { + ForgeListrOptions, + ForgeListrTaskFn, +} from '@electron-forge/shared-types'; import baseTemplate from '@electron-forge/template-base'; +import { autoTrace } from '@electron-forge/tracer'; import chalk from 'chalk'; import debug from 'debug'; import fs from 'fs-extra'; @@ -68,305 +72,329 @@ export interface ImportOptions { const readRawPackageJson = async (dir: string): Promise => fs.readJson(path.resolve(dir, 'package.json')); -export async function forgeImport({ - dir = process.cwd(), - interactive = false, - confirmImport, - shouldContinueOnExisting, - shouldRemoveDependency, - shouldUpdateScript, - outDir, - skipGit = false, -}: ImportOptions): Promise { - const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { - concurrent: false, - rendererOptions: { - collapseSubtasks: false, - collapseErrors: false, - }, - silentRendererCondition: !interactive, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), - }; - - const runner = new Listr( - [ - { - title: 'Locating importable project', - task: async () => { - d(`Attempting to import project in: ${dir}`); - if ( - !(await fs.pathExists(dir)) || - !(await fs.pathExists(path.resolve(dir, 'package.json'))) - ) { - throw new Error( - `We couldn't find a project with a package.json file in: ${dir}`, - ); - } - - if (typeof confirmImport === 'function') { - if (!(await confirmImport())) { - // TODO: figure out if we can just return early here - // eslint-disable-next-line no-process-exit - process.exit(0); - } - } - - if (!skipGit) { - await initGit(dir); - } - }, +export const forgeImport = autoTrace( + { name: 'import()', category: 'create-electron-app' }, + async ( + childTrace, + { + dir = process.cwd(), + interactive = false, + confirmImport, + shouldContinueOnExisting, + shouldRemoveDependency, + shouldUpdateScript, + outDir, + skipGit = false, + }: ImportOptions, + ): Promise => { + const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { + concurrent: false, + rendererOptions: { + collapseSubtasks: false, + collapseErrors: false, }, - { - title: 'Processing configuration and dependencies', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, - }, - task: async (_ctx, task) => { - const calculatedOutDir = outDir || 'out'; + silentRendererCondition: !interactive, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }; - const importDeps = ([] as string[]).concat(deps); - let importDevDeps = ([] as string[]).concat(devDeps); - let importExactDevDeps = ([] as string[]).concat(exactDevDeps); + const runner = new Listr( + [ + { + title: 'Locating importable project', + task: childTrace( + { name: 'locate-project', category: 'create-electron-app' }, + async () => { + d(`Attempting to import project in: ${dir}`); + if ( + !(await fs.pathExists(dir)) || + !(await fs.pathExists(path.resolve(dir, 'package.json'))) + ) { + throw new Error( + `We couldn't find a project with a package.json file in: ${dir}`, + ); + } - let packageJSON = await readRawPackageJson(dir); - if (!packageJSON.version) { - task.output = chalk.yellow( - `Please set the ${chalk.green('"version"')} in your application's package.json`, - ); - } - if (packageJSON.config && packageJSON.config.forge) { - if (packageJSON.config.forge.makers) { - task.output = chalk.green( - 'Existing Electron Forge configuration detected', - ); - if (typeof shouldContinueOnExisting === 'function') { - if (!(await shouldContinueOnExisting())) { + if (typeof confirmImport === 'function') { + if (!(await confirmImport())) { // TODO: figure out if we can just return early here // eslint-disable-next-line no-process-exit process.exit(0); } } - } else if (!(typeof packageJSON.config.forge === 'object')) { - task.output = chalk.yellow( - "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", - ); - } - } - - packageJSON.dependencies = packageJSON.dependencies || {}; - packageJSON.devDependencies = packageJSON.devDependencies || {}; - [importDevDeps, importExactDevDeps] = updateElectronDependency( - packageJSON, - importDevDeps, - importExactDevDeps, - ); + if (!skipGit) { + await initGit(dir); + } + }, + ), + }, + { + title: 'Processing configuration and dependencies', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: childTrace>( + { name: 'process-config', category: 'create-electron-app' }, + async (_, _ctx, task) => { + const calculatedOutDir = outDir || 'out'; - const keys = Object.keys(packageJSON.dependencies).concat( - Object.keys(packageJSON.devDependencies), - ); - const buildToolPackages: Record = { - '@electron/get': - 'already uses this module as a transitive dependency', - '@electron/osx-sign': - 'already uses this module as a transitive dependency', - '@electron/packager': - 'already uses this module as a transitive dependency', - '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', - 'electron-installer-redhat': - 'already uses this module as a transitive dependency', - 'electron-winstaller': - 'already uses this module as a transitive dependency', - }; + const importDeps = ([] as string[]).concat(deps); + let importDevDeps = ([] as string[]).concat(devDeps); + let importExactDevDeps = ([] as string[]).concat(exactDevDeps); - for (const key of keys) { - if (buildToolPackages[key]) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const explanation = buildToolPackages[key]!; - let remove = true; - if (typeof shouldRemoveDependency === 'function') { - remove = await shouldRemoveDependency(key, explanation); + let packageJSON = await readRawPackageJson(dir); + if (!packageJSON.version) { + task.output = chalk.yellow( + `Please set the ${chalk.green('"version"')} in your application's package.json`, + ); } - - if (remove) { - delete packageJSON.dependencies[key]; - delete packageJSON.devDependencies[key]; + if (packageJSON.config && packageJSON.config.forge) { + if (packageJSON.config.forge.makers) { + task.output = chalk.green( + 'Existing Electron Forge configuration detected', + ); + if (typeof shouldContinueOnExisting === 'function') { + if (!(await shouldContinueOnExisting())) { + // TODO: figure out if we can just return early here + // eslint-disable-next-line no-process-exit + process.exit(0); + } + } + } else if (!(typeof packageJSON.config.forge === 'object')) { + task.output = chalk.yellow( + "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", + ); + } } - } - } - packageJSON.scripts = packageJSON.scripts || {}; - d('reading current scripts object:', packageJSON.scripts); + packageJSON.dependencies = packageJSON.dependencies || {}; + packageJSON.devDependencies = packageJSON.devDependencies || {}; - const updatePackageScript = async ( - scriptName: string, - newValue: string, - ) => { - if (packageJSON.scripts[scriptName] !== newValue) { - let update = true; - if (typeof shouldUpdateScript === 'function') { - update = await shouldUpdateScript(scriptName, newValue); - } - if (update) { - packageJSON.scripts[scriptName] = newValue; - } - } - }; + [importDevDeps, importExactDevDeps] = updateElectronDependency( + packageJSON, + importDevDeps, + importExactDevDeps, + ); + + const keys = Object.keys(packageJSON.dependencies).concat( + Object.keys(packageJSON.devDependencies), + ); + const buildToolPackages: Record = { + '@electron/get': + 'already uses this module as a transitive dependency', + '@electron/osx-sign': + 'already uses this module as a transitive dependency', + '@electron/packager': + 'already uses this module as a transitive dependency', + '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', + 'electron-installer-redhat': + 'already uses this module as a transitive dependency', + 'electron-winstaller': + 'already uses this module as a transitive dependency', + }; - await updatePackageScript('start', 'electron-forge start'); - await updatePackageScript('package', 'electron-forge package'); - await updatePackageScript('make', 'electron-forge make'); + for (const key of keys) { + if (buildToolPackages[key]) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const explanation = buildToolPackages[key]!; + let remove = true; + if (typeof shouldRemoveDependency === 'function') { + remove = await shouldRemoveDependency(key, explanation); + } - d('forgified scripts object:', packageJSON.scripts); + if (remove) { + delete packageJSON.dependencies[key]; + delete packageJSON.devDependencies[key]; + } + } + } - const writeChanges = async () => { - await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { - spaces: 2, - }); - }; + packageJSON.scripts = packageJSON.scripts || {}; + d('reading current scripts object:', packageJSON.scripts); - return task.newListr( - [ - { - title: `Resolving package manager`, - task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(); - task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; - }, - }, - { - title: 'Configuring Yarn (if applicable)', - task: async ({ pm }) => { - // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config - // lets ensure that nodeLinker is set to node-modules - if (pm.executable === 'yarn') { - const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); - if (!(await fs.pathExists(yarnrcPath))) { - d('creating .yarnrc.yml with nodeLinker: node-modules'); - await fs.writeFile( - yarnrcPath, - 'nodeLinker: node-modules\n', - ); - } + const updatePackageScript = async ( + scriptName: string, + newValue: string, + ) => { + if (packageJSON.scripts[scriptName] !== newValue) { + let update = true; + if (typeof shouldUpdateScript === 'function') { + update = await shouldUpdateScript(scriptName, newValue); + } + if (update) { + packageJSON.scripts[scriptName] = newValue; } - }, - }, - { - title: 'Installing dependencies', - task: async ({ pm }, task) => { - await writeChanges(); + } + }; - d('deleting old dependencies forcefully'); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron'), - ); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron.cmd'), - ); + await updatePackageScript('start', 'electron-forge start'); + await updatePackageScript('package', 'electron-forge package'); + await updatePackageScript('make', 'electron-forge make'); - d('installing dependencies'); - task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; - await installDependencies(pm, dir, importDeps); + d('forgified scripts object:', packageJSON.scripts); - d('installing devDependencies'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; - await installDependencies( - pm, - dir, - importDevDeps, - DepType.DEV, - ); + const writeChanges = async () => { + await fs.writeJson( + path.resolve(dir, 'package.json'), + packageJSON, + { spaces: 2 }, + ); + }; - d('installing devDependencies with exact versions'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${importExactDevDeps.join(' ')}`; - await installDependencies( - pm, - dir, - importExactDevDeps, - DepType.DEV, - DepVersionRestriction.EXACT, - ); - }, - }, - { - title: 'Copying base template Forge configuration', - task: async () => { - const pathToTemplateConfig = path.resolve( - baseTemplate.templateDir, - 'forge.config.js', - ); + return task.newListr( + [ + { + title: `Resolving package manager`, + task: async (ctx, task) => { + ctx.pm = await resolvePackageManager(); + task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; + }, + }, + { + title: 'Configuring Yarn (if applicable)', + task: async ({ pm }) => { + // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config + // lets ensure that nodeLinker is set to node-modules + if (pm.executable === 'yarn') { + const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); + if (!(await fs.pathExists(yarnrcPath))) { + d( + 'creating .yarnrc.yml with nodeLinker: node-modules', + ); + await fs.writeFile( + yarnrcPath, + 'nodeLinker: node-modules\n', + ); + } + } + }, + }, + { + title: 'Installing dependencies', + task: async ({ pm }, task) => { + await writeChanges(); - // if there's an existing config.forge object in package.json - if ( - packageJSON?.config?.forge && - typeof packageJSON.config.forge === 'object' - ) { - d( - 'detected existing Forge config in package.json, merging with base template Forge config', - ); - const templateConfig = await import( - path.resolve(baseTemplate.templateDir, 'forge.config.js') - ); - packageJSON = await readRawPackageJson(dir); - merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object - await writeChanges(); - // otherwise, write to forge.config.js - } else { - d('writing new forge.config.js'); - await fs.copyFile( - pathToTemplateConfig, - path.resolve(dir, 'forge.config.js'), - ); - } - }, - }, - { - title: 'Fixing .gitignore', - task: async () => { - if (await fs.pathExists(path.resolve(dir, '.gitignore'))) { - const gitignore = await fs.readFile( - path.resolve(dir, '.gitignore'), - ); - if (!gitignore.includes(calculatedOutDir)) { - await fs.writeFile( - path.resolve(dir, '.gitignore'), - `${gitignore}\n${calculatedOutDir}/`, + d('deleting old dependencies forcefully'); + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron'), ); - } - } - }, - }, - ], - { concurrent: listrOptions.concurrent }, - ); - }, - }, - { - title: 'Finalizing import', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron.cmd'), + ); + + d('installing dependencies'); + task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; + await installDependencies(pm, dir, importDeps); + + d('installing devDependencies'); + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; + 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 installDependencies( + pm, + dir, + importExactDevDeps, + DepType.DEV, + DepVersionRestriction.EXACT, + ); + }, + }, + { + title: 'Copying base template Forge configuration', + task: async () => { + const pathToTemplateConfig = path.resolve( + baseTemplate.templateDir, + 'forge.config.js', + ); + + // if there's an existing config.forge object in package.json + if ( + packageJSON?.config?.forge && + typeof packageJSON.config.forge === 'object' + ) { + d( + 'detected existing Forge config in package.json, merging with base template Forge config', + ); + const templateConfig = await import( + path.resolve( + baseTemplate.templateDir, + 'forge.config.js', + ) + ); + packageJSON = await readRawPackageJson(dir); + merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object + await writeChanges(); + // otherwise, write to forge.config.js + } else { + d('writing new forge.config.js'); + await fs.copyFile( + pathToTemplateConfig, + path.resolve(dir, 'forge.config.js'), + ); + } + }, + }, + { + title: 'Fixing .gitignore', + task: async () => { + if ( + await fs.pathExists(path.resolve(dir, '.gitignore')) + ) { + const gitignore = await fs.readFile( + path.resolve(dir, '.gitignore'), + ); + if (!gitignore.includes(calculatedOutDir)) { + await fs.writeFile( + path.resolve(dir, '.gitignore'), + `${gitignore}\n${calculatedOutDir}/`, + ); + } + } + }, + }, + ], + { concurrent: listrOptions.concurrent }, + ); + }, + ), }, - task: (_ctx, task) => { - task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. + { + title: 'Finalizing import', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: childTrace>( + { name: 'finalize-import', category: 'create-electron-app' }, + (_, __, task) => { + task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. Thanks for using ${chalk.green('Electron Forge')}!`; + }, + ), }, - }, - ], - listrOptions, - ); + ], + listrOptions, + ); - await runner.run(); -} + await runner.run(); + }, +); From 5b57a9638ec8a355a14d987a97d9bedab792e426 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 13:31:47 -0800 Subject: [PATCH 24/27] fixup lockfile --- packages/external/create-electron-app/package.json | 2 +- yarn.lock | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 90602fb1ed..f84556c3e6 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -12,11 +12,11 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@electron-forge/template-base": "workspace:*", - "@electron-forge/tracer": "workspace:*", "@electron-forge/template-vite": "workspace:*", "@electron-forge/template-vite-typescript": "workspace:*", "@electron-forge/template-webpack": "workspace:*", "@electron-forge/template-webpack-typescript": "workspace:*", + "@electron-forge/tracer": "workspace:*", "@inquirer/prompts": "^6.0.1", "@listr2/prompt-adapter-inquirer": "^2.0.22", "@malept/cross-spawn-promise": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 627fa67618..f4a0ff019a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7975,6 +7975,7 @@ __metadata: "@electron-forge/template-vite-typescript": "workspace:*" "@electron-forge/template-webpack": "workspace:*" "@electron-forge/template-webpack-typescript": "workspace:*" + "@electron-forge/tracer": "workspace:*" "@inquirer/prompts": "npm:^6.0.1" "@listr2/prompt-adapter-inquirer": "npm:^2.0.22" "@malept/cross-spawn-promise": "npm:^2.0.0" From 66520831d83fcc0a5539ac5fb59cf2e9703a897c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 14:07:31 -0800 Subject: [PATCH 25/27] fix faulty import --- packages/external/create-electron-app/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external/create-electron-app/src/index.ts b/packages/external/create-electron-app/src/index.ts index 0f7cfc7f9f..33401ad6d4 100644 --- a/packages/external/create-electron-app/src/index.ts +++ b/packages/external/create-electron-app/src/index.ts @@ -36,4 +36,4 @@ process.on('uncaughtException', (err) => { process.exit(1); }); -import './create-electron-app'; +import './create-electron-app.js'; From 87e26dd82576c56fe3121a941606708ee7d8d43e Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 14:07:59 -0800 Subject: [PATCH 26/27] remove tracer --- .../external/create-electron-app/package.json | 1 - .../create-electron-app/src/import.ts | 572 +++++++++--------- 2 files changed, 272 insertions(+), 301 deletions(-) diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index f84556c3e6..16f129ad3d 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -16,7 +16,6 @@ "@electron-forge/template-vite-typescript": "workspace:*", "@electron-forge/template-webpack": "workspace:*", "@electron-forge/template-webpack-typescript": "workspace:*", - "@electron-forge/tracer": "workspace:*", "@inquirer/prompts": "^6.0.1", "@listr2/prompt-adapter-inquirer": "^2.0.22", "@malept/cross-spawn-promise": "^2.0.0", diff --git a/packages/external/create-electron-app/src/import.ts b/packages/external/create-electron-app/src/import.ts index 89ecc35f59..f2b032ef4b 100644 --- a/packages/external/create-electron-app/src/import.ts +++ b/packages/external/create-electron-app/src/import.ts @@ -8,12 +8,8 @@ import { resolvePackageManager, updateElectronDependency, } from '@electron-forge/core-utils'; -import { - ForgeListrOptions, - ForgeListrTaskFn, -} from '@electron-forge/shared-types'; +import { ForgeListrOptions } from '@electron-forge/shared-types'; import baseTemplate from '@electron-forge/template-base'; -import { autoTrace } from '@electron-forge/tracer'; import chalk from 'chalk'; import debug from 'debug'; import fs from 'fs-extra'; @@ -72,329 +68,305 @@ export interface ImportOptions { const readRawPackageJson = async (dir: string): Promise => fs.readJson(path.resolve(dir, 'package.json')); -export const forgeImport = autoTrace( - { name: 'import()', category: 'create-electron-app' }, - async ( - childTrace, - { - dir = process.cwd(), - interactive = false, - confirmImport, - shouldContinueOnExisting, - shouldRemoveDependency, - shouldUpdateScript, - outDir, - skipGit = false, - }: ImportOptions, - ): Promise => { - const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { - concurrent: false, - rendererOptions: { - collapseSubtasks: false, - collapseErrors: false, +export const forgeImport = async ({ + dir = process.cwd(), + interactive = false, + confirmImport, + shouldContinueOnExisting, + shouldRemoveDependency, + shouldUpdateScript, + outDir, + skipGit = false, +}: ImportOptions): Promise => { + const listrOptions: ForgeListrOptions<{ pm: PMDetails }> = { + concurrent: false, + rendererOptions: { + collapseSubtasks: false, + collapseErrors: false, + }, + silentRendererCondition: !interactive, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }; + + const runner = new Listr( + [ + { + title: 'Locating importable project', + task: async () => { + d(`Attempting to import project in: ${dir}`); + if ( + !(await fs.pathExists(dir)) || + !(await fs.pathExists(path.resolve(dir, 'package.json'))) + ) { + throw new Error( + `We couldn't find a project with a package.json file in: ${dir}`, + ); + } + + if (typeof confirmImport === 'function') { + if (!(await confirmImport())) { + // TODO: figure out if we can just return early here + // eslint-disable-next-line no-process-exit + process.exit(0); + } + } + + if (!skipGit) { + await initGit(dir); + } + }, }, - silentRendererCondition: !interactive, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), - }; + { + title: 'Processing configuration and dependencies', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: async (_ctx, task) => { + const calculatedOutDir = outDir || 'out'; - const runner = new Listr( - [ - { - title: 'Locating importable project', - task: childTrace( - { name: 'locate-project', category: 'create-electron-app' }, - async () => { - d(`Attempting to import project in: ${dir}`); - if ( - !(await fs.pathExists(dir)) || - !(await fs.pathExists(path.resolve(dir, 'package.json'))) - ) { - throw new Error( - `We couldn't find a project with a package.json file in: ${dir}`, - ); - } + const importDeps = ([] as string[]).concat(deps); + let importDevDeps = ([] as string[]).concat(devDeps); + let importExactDevDeps = ([] as string[]).concat(exactDevDeps); - if (typeof confirmImport === 'function') { - if (!(await confirmImport())) { + let packageJSON = await readRawPackageJson(dir); + if (!packageJSON.version) { + task.output = chalk.yellow( + `Please set the ${chalk.green('"version"')} in your application's package.json`, + ); + } + if (packageJSON.config && packageJSON.config.forge) { + if (packageJSON.config.forge.makers) { + task.output = chalk.green( + 'Existing Electron Forge configuration detected', + ); + if (typeof shouldContinueOnExisting === 'function') { + if (!(await shouldContinueOnExisting())) { // TODO: figure out if we can just return early here // eslint-disable-next-line no-process-exit process.exit(0); } } + } else if (!(typeof packageJSON.config.forge === 'object')) { + task.output = chalk.yellow( + "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", + ); + } + } - if (!skipGit) { - await initGit(dir); - } - }, - ), - }, - { - title: 'Processing configuration and dependencies', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, - }, - task: childTrace>( - { name: 'process-config', category: 'create-electron-app' }, - async (_, _ctx, task) => { - const calculatedOutDir = outDir || 'out'; - - const importDeps = ([] as string[]).concat(deps); - let importDevDeps = ([] as string[]).concat(devDeps); - let importExactDevDeps = ([] as string[]).concat(exactDevDeps); + packageJSON.dependencies = packageJSON.dependencies || {}; + packageJSON.devDependencies = packageJSON.devDependencies || {}; - let packageJSON = await readRawPackageJson(dir); - if (!packageJSON.version) { - task.output = chalk.yellow( - `Please set the ${chalk.green('"version"')} in your application's package.json`, - ); - } - if (packageJSON.config && packageJSON.config.forge) { - if (packageJSON.config.forge.makers) { - task.output = chalk.green( - 'Existing Electron Forge configuration detected', - ); - if (typeof shouldContinueOnExisting === 'function') { - if (!(await shouldContinueOnExisting())) { - // TODO: figure out if we can just return early here - // eslint-disable-next-line no-process-exit - process.exit(0); - } - } - } else if (!(typeof packageJSON.config.forge === 'object')) { - task.output = chalk.yellow( - "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", - ); - } - } + [importDevDeps, importExactDevDeps] = updateElectronDependency( + packageJSON, + importDevDeps, + importExactDevDeps, + ); - packageJSON.dependencies = packageJSON.dependencies || {}; - packageJSON.devDependencies = packageJSON.devDependencies || {}; + const keys = Object.keys(packageJSON.dependencies).concat( + Object.keys(packageJSON.devDependencies), + ); + const buildToolPackages: Record = { + '@electron/get': + 'already uses this module as a transitive dependency', + '@electron/osx-sign': + 'already uses this module as a transitive dependency', + '@electron/packager': + 'already uses this module as a transitive dependency', + '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', + 'electron-installer-redhat': + 'already uses this module as a transitive dependency', + 'electron-winstaller': + 'already uses this module as a transitive dependency', + }; - [importDevDeps, importExactDevDeps] = updateElectronDependency( - packageJSON, - importDevDeps, - importExactDevDeps, - ); + for (const key of keys) { + if (buildToolPackages[key]) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const explanation = buildToolPackages[key]!; + let remove = true; + if (typeof shouldRemoveDependency === 'function') { + remove = await shouldRemoveDependency(key, explanation); + } - const keys = Object.keys(packageJSON.dependencies).concat( - Object.keys(packageJSON.devDependencies), - ); - const buildToolPackages: Record = { - '@electron/get': - 'already uses this module as a transitive dependency', - '@electron/osx-sign': - 'already uses this module as a transitive dependency', - '@electron/packager': - 'already uses this module as a transitive dependency', - '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', - 'electron-installer-redhat': - 'already uses this module as a transitive dependency', - 'electron-winstaller': - 'already uses this module as a transitive dependency', - }; + if (remove) { + delete packageJSON.dependencies[key]; + delete packageJSON.devDependencies[key]; + } + } + } - for (const key of keys) { - if (buildToolPackages[key]) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const explanation = buildToolPackages[key]!; - let remove = true; - if (typeof shouldRemoveDependency === 'function') { - remove = await shouldRemoveDependency(key, explanation); - } + packageJSON.scripts = packageJSON.scripts || {}; + d('reading current scripts object:', packageJSON.scripts); - if (remove) { - delete packageJSON.dependencies[key]; - delete packageJSON.devDependencies[key]; - } - } + const updatePackageScript = async ( + scriptName: string, + newValue: string, + ) => { + if (packageJSON.scripts[scriptName] !== newValue) { + let update = true; + if (typeof shouldUpdateScript === 'function') { + update = await shouldUpdateScript(scriptName, newValue); } + if (update) { + packageJSON.scripts[scriptName] = newValue; + } + } + }; - packageJSON.scripts = packageJSON.scripts || {}; - d('reading current scripts object:', packageJSON.scripts); + await updatePackageScript('start', 'electron-forge start'); + await updatePackageScript('package', 'electron-forge package'); + await updatePackageScript('make', 'electron-forge make'); - const updatePackageScript = async ( - scriptName: string, - newValue: string, - ) => { - if (packageJSON.scripts[scriptName] !== newValue) { - let update = true; - if (typeof shouldUpdateScript === 'function') { - update = await shouldUpdateScript(scriptName, newValue); - } - if (update) { - packageJSON.scripts[scriptName] = newValue; - } - } - }; + d('forgified scripts object:', packageJSON.scripts); - await updatePackageScript('start', 'electron-forge start'); - await updatePackageScript('package', 'electron-forge package'); - await updatePackageScript('make', 'electron-forge make'); + const writeChanges = async () => { + await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { + spaces: 2, + }); + }; - d('forgified scripts object:', packageJSON.scripts); - - const writeChanges = async () => { - await fs.writeJson( - path.resolve(dir, 'package.json'), - packageJSON, - { spaces: 2 }, - ); - }; + return task.newListr( + [ + { + title: `Resolving package manager`, + task: async (ctx, task) => { + ctx.pm = await resolvePackageManager(); + task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; + }, + }, + { + title: 'Configuring Yarn (if applicable)', + task: async ({ pm }) => { + // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config + // lets ensure that nodeLinker is set to node-modules + if (pm.executable === 'yarn') { + const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); + if (!(await fs.pathExists(yarnrcPath))) { + d('creating .yarnrc.yml with nodeLinker: node-modules'); + await fs.writeFile( + yarnrcPath, + 'nodeLinker: node-modules\n', + ); + } + } + }, + }, + { + title: 'Installing dependencies', + task: async ({ pm }, task) => { + await writeChanges(); - return task.newListr( - [ - { - title: `Resolving package manager`, - task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(); - task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; - }, - }, - { - title: 'Configuring Yarn (if applicable)', - task: async ({ pm }) => { - // Yarn v4 defaults to PnP which doesn't work well with CommonJS requires in our forge config - // lets ensure that nodeLinker is set to node-modules - if (pm.executable === 'yarn') { - const yarnrcPath = path.resolve(dir, '.yarnrc.yml'); - if (!(await fs.pathExists(yarnrcPath))) { - d( - 'creating .yarnrc.yml with nodeLinker: node-modules', - ); - await fs.writeFile( - yarnrcPath, - 'nodeLinker: node-modules\n', - ); - } - } - }, - }, - { - title: 'Installing dependencies', - task: async ({ pm }, task) => { - await writeChanges(); + d('deleting old dependencies forcefully'); + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron'), + ); + await fs.remove( + path.resolve(dir, 'node_modules/.bin/electron.cmd'), + ); - d('deleting old dependencies forcefully'); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron'), - ); - await fs.remove( - path.resolve(dir, 'node_modules/.bin/electron.cmd'), - ); + d('installing dependencies'); + task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; + await installDependencies(pm, dir, importDeps); - d('installing dependencies'); - task.output = `${pm.executable} ${pm.install} ${importDeps.join(' ')}`; - await installDependencies(pm, dir, importDeps); + d('installing devDependencies'); + task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; + await installDependencies( + pm, + dir, + importDevDeps, + DepType.DEV, + ); - d('installing devDependencies'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${importDevDeps.join(' ')}`; - 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 installDependencies( + pm, + dir, + importExactDevDeps, + DepType.DEV, + DepVersionRestriction.EXACT, + ); + }, + }, + { + title: 'Copying base template Forge configuration', + task: async () => { + const pathToTemplateConfig = path.resolve( + baseTemplate.templateDir, + 'forge.config.js', + ); - d('installing devDependencies with exact versions'); - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${pm.exact} ${importExactDevDeps.join(' ')}`; - await installDependencies( - pm, - dir, - importExactDevDeps, - DepType.DEV, - DepVersionRestriction.EXACT, - ); - }, - }, - { - title: 'Copying base template Forge configuration', - task: async () => { - const pathToTemplateConfig = path.resolve( - baseTemplate.templateDir, - 'forge.config.js', + // if there's an existing config.forge object in package.json + if ( + packageJSON?.config?.forge && + typeof packageJSON.config.forge === 'object' + ) { + d( + 'detected existing Forge config in package.json, merging with base template Forge config', + ); + const templateConfig = await import( + path.resolve(baseTemplate.templateDir, 'forge.config.js') + ); + packageJSON = await readRawPackageJson(dir); + merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object + await writeChanges(); + // otherwise, write to forge.config.js + } else { + d('writing new forge.config.js'); + await fs.copyFile( + pathToTemplateConfig, + path.resolve(dir, 'forge.config.js'), + ); + } + }, + }, + { + title: 'Fixing .gitignore', + task: async () => { + if (await fs.pathExists(path.resolve(dir, '.gitignore'))) { + const gitignore = await fs.readFile( + path.resolve(dir, '.gitignore'), + ); + if (!gitignore.includes(calculatedOutDir)) { + await fs.writeFile( + path.resolve(dir, '.gitignore'), + `${gitignore}\n${calculatedOutDir}/`, ); - - // if there's an existing config.forge object in package.json - if ( - packageJSON?.config?.forge && - typeof packageJSON.config.forge === 'object' - ) { - d( - 'detected existing Forge config in package.json, merging with base template Forge config', - ); - const templateConfig = await import( - path.resolve( - baseTemplate.templateDir, - 'forge.config.js', - ) - ); - packageJSON = await readRawPackageJson(dir); - merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object - await writeChanges(); - // otherwise, write to forge.config.js - } else { - d('writing new forge.config.js'); - await fs.copyFile( - pathToTemplateConfig, - path.resolve(dir, 'forge.config.js'), - ); - } - }, - }, - { - title: 'Fixing .gitignore', - task: async () => { - if ( - await fs.pathExists(path.resolve(dir, '.gitignore')) - ) { - const gitignore = await fs.readFile( - path.resolve(dir, '.gitignore'), - ); - if (!gitignore.includes(calculatedOutDir)) { - await fs.writeFile( - path.resolve(dir, '.gitignore'), - `${gitignore}\n${calculatedOutDir}/`, - ); - } - } - }, - }, - ], - { concurrent: listrOptions.concurrent }, - ); - }, - ), + } + } + }, + }, + ], + { concurrent: listrOptions.concurrent }, + ); }, - { - title: 'Finalizing import', - rendererOptions: { - persistentOutput: true, - bottomBar: Infinity, - }, - task: childTrace>( - { name: 'finalize-import', category: 'create-electron-app' }, - (_, __, task) => { - task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. + }, + { + title: 'Finalizing import', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: (_ctx, task) => { + task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. Thanks for using ${chalk.green('Electron Forge')}!`; - }, - ), }, - ], - listrOptions, - ); + }, + ], + listrOptions, + ); - await runner.run(); - }, -); + await runner.run(); +}; From 4d185cd0d5ed212802cf4d4f072f26893cd8de7b Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 6 Mar 2026 14:24:27 -0800 Subject: [PATCH 27/27] lockfile --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index f4a0ff019a..627fa67618 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7975,7 +7975,6 @@ __metadata: "@electron-forge/template-vite-typescript": "workspace:*" "@electron-forge/template-webpack": "workspace:*" "@electron-forge/template-webpack-typescript": "workspace:*" - "@electron-forge/tracer": "workspace:*" "@inquirer/prompts": "npm:^6.0.1" "@listr2/prompt-adapter-inquirer": "npm:^2.0.22" "@malept/cross-spawn-promise": "npm:^2.0.0"