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/api/cli/package.json b/packages/api/cli/package.json index d9530cdda2..6ff5bccde0 100644 --- a/packages/api/cli/package.json +++ b/packages/api/cli/package.json @@ -20,8 +20,6 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@electron/get": "^4.0.2", - "@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/cli/src/electron-forge-import.ts b/packages/api/cli/src/electron-forge-import.ts deleted file mode 100644 index 0380167518..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.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.') - .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-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/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 792788d49f..5afc357f23 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -26,8 +26,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('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 7995edeb1d..2a9989f28e 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": "^4.0.2", "@electron/packager": "^19.0.1", @@ -53,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/api/core/spec/fast/publish.spec.ts b/packages/api/core/spec/fast/publish.spec.ts index d5c714efa7..8f9540944c 100644 --- a/packages/api/core/spec/fast/publish.spec.ts +++ b/packages/api/core/spec/fast/publish.spec.ts @@ -11,7 +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.js'; -import importSearch from '../../src/util/import-search.js'; +import { importSearch } from '../../src/util/import-search.js'; vi.mock(import('../../src/api/make'), async (importOriginal) => { const mod = await importOriginal(); @@ -41,7 +41,7 @@ vi.mock(import('../../src/util/import-search'), async (importOriginal) => { const mod = await importOriginal(); return { ...mod, - default: vi.fn(), + importSearch: vi.fn(), }; }); diff --git a/packages/api/core/spec/fast/util/import-search.spec.ts b/packages/api/core/spec/fast/util/import-search.spec.ts index d033a54d62..7f6123d6ba 100644 --- a/packages/api/core/spec/fast/util/import-search.spec.ts +++ b/packages/api/core/spec/fast/util/import-search.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import findConfig from '../../../src/util/forge-config.js'; -import importSearch from '../../../src/util/import-search.js'; +import { importSearch } from '../../../src/util/import-search.js'; describe('import-search', () => { it('should resolve null if no file exists', async () => { 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 d8dc27671a..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.js'; - 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 deleted file mode 100644 index 2b12daa077..0000000000 --- a/packages/api/core/src/api/import.ts +++ /dev/null @@ -1,400 +0,0 @@ -import path from 'node:path'; - -import { - 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-es'; - -import { - DepType, - DepVersionRestriction, - installDependencies, -} from '../util/install-dependencies.js'; -import { readRawPackageJson } from '../util/read-package-json.js'; - -import { initGit } from './init-scripts/init-git.js'; -import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm.js'; - -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', - ); - 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}/`, - ); - } - } - }, - }, - ], - 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 68299e7525..86b4768fa6 100644 --- a/packages/api/core/src/api/index.ts +++ b/packages/api/core/src/api/index.ts @@ -2,31 +2,12 @@ import { ElectronProcess, ForgeMakeResult } from '@electron-forge/shared-types'; import ForgeUtils from '../util/index.js'; -import _import, { ImportOptions } from './import.js'; -import init, { InitOptions } from './init.js'; import make, { MakeOptions } from './make.js'; import _package, { PackageOptions } from './package.js'; import publish, { PublishOptions } from './publish.js'; import start, { StartOptions } from './start.js'; 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 */ @@ -65,8 +46,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 9e20404df5..97608874fd 100644 --- a/packages/api/core/src/api/make.ts +++ b/packages/api/core/src/api/make.ts @@ -22,7 +22,7 @@ import logSymbols from 'log-symbols'; import getForgeConfig from '../util/forge-config.js'; import { getHookListrTasks, runMutatingHook } from '../util/hook.js'; -import importSearch from '../util/import-search.js'; +import { importSearch } from '../util/import-search.js'; import getCurrentOutDir from '../util/out-dir.js'; import parseArchs from '../util/parse-archs.js'; import { readMutatedPackageJson } from '../util/read-package-json.js'; diff --git a/packages/api/core/src/api/package.ts b/packages/api/core/src/api/package.ts index 44dfe0ca86..cd9d777bc9 100644 --- a/packages/api/core/src/api/package.ts +++ b/packages/api/core/src/api/package.ts @@ -30,7 +30,7 @@ import { Listr, PRESET_TIMER } from 'listr2'; import getForgeConfig from '../util/forge-config.js'; import { getHookListrTasks, runHook } from '../util/hook.js'; -import importSearch from '../util/import-search.js'; +import { importSearch } from '../util/import-search.js'; import { warn } from '../util/messages.js'; import getCurrentOutDir from '../util/out-dir.js'; import { readMutatedPackageJson } from '../util/read-package-json.js'; diff --git a/packages/api/core/src/api/publish.ts b/packages/api/core/src/api/publish.ts index 31abf360e4..e338220ab7 100644 --- a/packages/api/core/src/api/publish.ts +++ b/packages/api/core/src/api/publish.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { Listr } from 'listr2'; import getForgeConfig from '../util/forge-config.js'; -import importSearch from '../util/import-search.js'; +import { importSearch } from '../util/import-search.js'; import getCurrentOutDir from '../util/out-dir.js'; import PublishState from '../util/publish-state.js'; import resolveDir from '../util/resolve-dir.js'; diff --git a/packages/api/core/src/util/import-search.ts b/packages/api/core/src/util/import-search.ts index a548c1e29e..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. @@ -93,12 +89,12 @@ export type PossibleModule = { * @returns The module's default export, or `null` if the module was not found * or has no default export. */ -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 : null; -}; +} diff --git a/packages/api/core/src/util/plugin-interface.ts b/packages/api/core/src/util/plugin-interface.ts index 77fc425ef5..69aa08e3da 100644 --- a/packages/api/core/src/util/plugin-interface.ts +++ b/packages/api/core/src/util/plugin-interface.ts @@ -16,7 +16,7 @@ import debug from 'debug'; import { StartOptions } from '../api/start.js'; -import importSearch from './import-search.js'; +import { importSearch } from './import-search.js'; const d = debug('electron-forge:plugins'); diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index fd3723da5d..672353e20d 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -8,8 +8,31 @@ "typings": "dist/index.d.ts", "author": "Samuel Attard", "license": "MIT", + "engines": { + "node": ">= 22.12.0" + }, "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-es": "^4.17.21", + "log-symbols": "^4.0.0", + "semver": "^7.2.1" + }, + "devDependencies": { + "@electron-forge/core": "workspace:*" }, "bin": "dist/index.js", "files": [ diff --git a/packages/api/core/spec/fast/find-template.spec.ts b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts similarity index 96% rename from packages/api/core/spec/fast/find-template.spec.ts rename to packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts index 6f05c334f1..c4412bb4d5 100644 --- a/packages/api/core/spec/fast/find-template.spec.ts +++ b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { findTemplate } from '../../src/api/init-scripts/find-template'; +import { findTemplate } from '../../../src/init-scripts/find-template'; describe('findTemplate', () => { /** 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 96% 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 373c192833..df8dfdb8f8 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.js'; +import { initGit } from '../../../src/init-scripts/init-git.js'; 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 86% 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 04fa5f1c8a..c2cddfbd7a 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 @@ -1,19 +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.js'; import { DepType, DepVersionRestriction, installDependencies, -} from '../../../src/util/install-dependencies.js'; + 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/init-scripts/init-npm.js'; -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/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/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..8a429c68c6 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'; 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/spec/slow/init.slow.verdaccio.spec.ts b/packages/external/create-electron-app/spec/slow/init.slow.verdaccio.spec.ts similarity index 93% 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 10be5f1b75..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 { api } from '../../src/api/index'; +import { init } from '../../src/init'; 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(import.meta.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( import.meta.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( import.meta.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/api/cli/src/electron-forge-init.ts b/packages/external/create-electron-app/src/create-electron-app.ts similarity index 86% rename from packages/api/cli/src/electron-forge-init.ts rename to packages/external/create-electron-app/src/create-electron-app.ts index 2811436eeb..b3cb52cc36 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -1,23 +1,27 @@ import fs from 'node:fs'; -import { api, InitOptions } from '@electron-forge/core'; +import { resolveWorkingDir } from '@electron-forge/core-utils'; 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.js'; import packageJSON from '../package.json' with { type: 'json' }; -import { resolveWorkingDir } from './util/resolve-working-dir.js'; +import { forgeImport } from './import.js'; +import { init, InitOptions } from './init.js'; // 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.') + .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.', @@ -38,7 +42,7 @@ program '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 options = initCommand.opts(); const tasks = new Listr( [ { @@ -173,7 +177,27 @@ program ); const initOpts: InitOptions = await tasks.run(); - await api.init(initOpts); + 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, false); + await forgeImport({ + dir: workingDir, + interactive: true, + skipGit: Boolean(options.skipGit), + }); }); program.parse(process.argv); diff --git a/packages/external/create-electron-app/src/import.ts b/packages/external/create-electron-app/src/import.ts new file mode 100644 index 0000000000..f2b032ef4b --- /dev/null +++ b/packages/external/create-electron-app/src/import.ts @@ -0,0 +1,372 @@ +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-es'; + +import { initGit } from './init-scripts/init-git.js'; +import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm.js'; + +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 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); + } + }, + }, + { + title: 'Processing configuration and dependencies', + rendererOptions: { + persistentOutput: true, + bottomBar: Infinity, + }, + task: 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( + [ + { + 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', + ); + 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 }, + ); + }, + }, + { + 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, + ); + + 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..33401ad6d4 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 './create-electron-app.js'; 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 93% 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 7fae645bc1..29e4d8df7b 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,11 +1,9 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import { ForgeTemplate } from '@electron-forge/shared-types'; +import { ForgeTemplate, PossibleModule } from '@electron-forge/shared-types'; import debug from 'debug'; -import { PossibleModule } from '../../util/import-search.js'; - 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 96% 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 f0afbae594..724f3d3eb3 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.js'; - const d = debug('electron-forge:init:link'); /** @@ -26,7 +24,11 @@ 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 raw = await fs.promises.readFile( + path.join(dir, 'package.json'), + 'utf-8', + ); + const packageJson = JSON.parse(raw); const forgeRoot = path.resolve( import.meta.dirname, '..', @@ -34,7 +36,6 @@ export async function initLink( '..', '..', '..', - '..', ); const getWorkspacePath = (packageName: string): string => { 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 88% 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 index 595d2e3f0c..e2e45accbd 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/external/create-electron-app/src/init-scripts/init-npm.ts @@ -1,24 +1,23 @@ 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.js'; + 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( - path.resolve(import.meta.dirname, '../../../package.json'), +const packageJSON = fs.readJsonSync( + path.resolve(import.meta.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/api/core/src/api/init.ts b/packages/external/create-electron-app/src/init.ts similarity index 93% rename from packages/api/core/src/api/init.ts rename to packages/external/create-electron-app/src/init.ts index e0c01e565d..5f29c79cd0 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -1,6 +1,14 @@ +import fs from 'node:fs'; import path from 'node:path'; -import { PMDetails, resolvePackageManager } from '@electron-forge/core-utils'; +import { + DepType, + DepVersionRestriction, + installDependencies, + PACKAGE_MANAGERS, + 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,13 +16,6 @@ import debug from 'debug'; import { Listr } from 'listr2'; import semver from 'semver'; -import { - DepType, - DepVersionRestriction, - installDependencies, -} from '../util/install-dependencies.js'; -import { readRawPackageJson } from '../util/read-package-json.js'; - import { findTemplate } from './init-scripts/find-template.js'; import { initDirectory } from './init-scripts/init-directory.js'; import { initGit } from './init-scripts/init-git.js'; @@ -71,9 +72,14 @@ async function validateTemplate( ); } - const forgeVersion = ( - await readRawPackageJson(path.join(import.meta.dirname, '..', '..')) - ).version; + const dir = path.join(import.meta.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}`, @@ -81,7 +87,7 @@ async function validateTemplate( } } -export default async ({ +export async function init({ dir = process.cwd(), interactive = false, copyCIFiles = false, @@ -90,7 +96,7 @@ export default async ({ skipGit = false, electronVersion = 'latest', packageManager, -}: InitOptions): Promise => { +}: InitOptions): Promise { d(`Initializing in: ${dir}`); const runner = new Listr<{ @@ -180,8 +186,8 @@ export default async ({ // 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}`; + task: async (ctx, task) => { + const pmString = `${ctx.pm.executable}@${ctx.pm.version}`; try { await spawn('corepack', ['use', pmString], { cwd: dir, @@ -189,6 +195,7 @@ export default async ({ task.title = `Set ${chalk.cyan(pmString)} via Corepack`; } catch (e) { d('corepack failed to run with error', e); + ctx.pm = { ...PACKAGE_MANAGERS['npm'] }; 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')}.`; } }, @@ -203,7 +210,7 @@ export default async ({ task: async ({ pm }, task) => { d('installing dependencies'); if (templateModule.dependencies?.length) { - task.output = `${pm.executable} ${pm.install} ${pm.dev} ${templateModule.dependencies.join(' ')}`; + task.output = `${pm.executable} ${pm.install} ${templateModule.dependencies.join(' ')}`; } return await installDependencies( pm, @@ -213,7 +220,6 @@ export default async ({ DepVersionRestriction.RANGE, ); }, - exitOnError: false, }, { title: 'Installing development dependencies', @@ -229,7 +235,6 @@ export default async ({ DepType.DEV, ); }, - exitOnError: false, }, { title: 'Finalizing dependencies', @@ -240,7 +245,6 @@ export default async ({ task: async ({ pm }, task) => { await initNPM(pm, dir, ctx.parsedElectronVersion, task); }, - exitOnError: false, }, { title: 'Linking Forge dependencies to local build', @@ -270,4 +274,4 @@ export default async ({ ); await runner.run(); -}; +} 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 f14a9a3de4..79420a2c08 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/init'; 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(import.meta.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 d0084fb290..43b11401d8 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/init'; 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(import.meta.dirname, '..'), interactive: false, diff --git a/packages/utils/core-utils/package.json b/packages/utils/core-utils/package.json index 5a154f24e0..cdf4b67f4f 100644 --- a/packages/utils/core-utils/package.json +++ b/packages/utils/core-utils/package.json @@ -28,6 +28,7 @@ }, "files": [ "dist", + "helper", "src" ] } diff --git a/packages/utils/core-utils/spec/electron-version.spec.ts b/packages/utils/core-utils/spec/electron-version.spec.ts index f1a9098311..f101612f2b 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.js'; +} from '../../../external/create-electron-app/src/init-scripts/init-npm.js'; import { getElectronModulePath, getElectronVersion, 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 92% rename from packages/api/core/spec/fast/util/install-dependencies.spec.ts rename to packages/utils/core-utils/spec/install-dependencies.spec.ts index a147a5aa78..c559634e4a 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,17 @@ -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.js'; +} from '../src/install-dependencies.js'; +import { + PACKAGE_MANAGERS, + spawnPackageManager, +} from '../src/package-manager.js'; -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/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 ac08af9b44..36865466bd 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -2,3 +2,5 @@ export * from './rebuild.js'; 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/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..3765f1c882 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.js'; + const d = debug('electron-forge:dependency-installer'); export enum DepType { diff --git a/packages/api/cli/src/util/resolve-working-dir.ts b/packages/utils/core-utils/src/resolve-working-dir.ts similarity index 95% rename from packages/api/cli/src/util/resolve-working-dir.ts rename to packages/utils/core-utils/src/resolve-working-dir.ts index a622543d15..6438d023cb 100644 --- a/packages/api/cli/src/util/resolve-working-dir.ts +++ b/packages/utils/core-utils/src/resolve-working-dir.ts @@ -1,7 +1,6 @@ +import fs from 'node:fs'; 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) diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index 047d6dcf28..43a68cff64 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -286,3 +286,7 @@ export type PackagePerson = email?: string; url?: string; }; + +export type PossibleModule = { + default?: T; +} & T; diff --git a/yarn.lock b/yarn.lock index 13f4878766..627fa67618 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: @@ -813,8 +813,6 @@ __metadata: "@electron-forge/core-utils": "workspace:*" "@electron-forge/shared-types": "workspace:*" "@electron/get": "npm:^4.0.2" - "@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" @@ -867,12 +865,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": "portal:./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:^4.0.2" @@ -893,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" @@ -1479,8 +1471,8 @@ __metadata: linkType: hard "@electron/lint-roller@npm:^3.1.3": - version: 3.2.0 - resolution: "@electron/lint-roller@npm:3.2.0" + version: 3.1.3 + resolution: "@electron/lint-roller@npm:3.1.3" dependencies: "@dsanders11/vscode-markdown-languageservice": "npm:^0.3.0" ajv: "npm:^8.16.0" @@ -1505,7 +1497,7 @@ __metadata: lint-roller-markdown-links: dist/bin/lint-markdown-links.js lint-roller-markdown-standard: dist/bin/lint-markdown-standard.js lint-roller-markdown-ts-check: dist/bin/lint-markdown-ts-check.js - checksum: 10c0/5331163518f41c1ea8ac302891d808891b5570f657587194717e3a8214c0d99e04949cf500ff138b02b77a373e317c3e77042b19c7fbda186d5c970b6b81df80 + checksum: 10c0/657b17b4921a8887334287e245dde85022704c673e1bba867e2a79abb643392b4e4e3962b8745074e78811938b28c4653c37b0c07a76a298a2a00ab3276f475e languageName: node linkType: hard @@ -7975,7 +7967,25 @@ __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": "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-es: "npm:^4.17.21" + log-symbols: "npm:^4.0.0" + semver: "npm:^7.2.1" bin: create-electron-app: dist/index.js languageName: unknown