diff --git a/packages/api/cli/spec/check-system.spec.ts b/packages/api/cli/spec/util/check-system.spec.ts similarity index 98% rename from packages/api/cli/spec/check-system.spec.ts rename to packages/api/cli/spec/util/check-system.spec.ts index 7cc30c3ea0..cb75c91aa5 100644 --- a/packages/api/cli/spec/check-system.spec.ts +++ b/packages/api/cli/spec/util/check-system.spec.ts @@ -1,7 +1,7 @@ import { resolvePackageManager, spawnPackageManager } from '@electron-forge/core-utils'; import { describe, expect, it, vi } from 'vitest'; -import { checkPackageManager } from '../src/util/check-system'; +import { checkPackageManager } from '../../src/util/check-system'; vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { const mod = await importOriginal(); diff --git a/packages/api/cli/spec/util/resolve-working-dir.spec.ts b/packages/api/cli/spec/util/resolve-working-dir.spec.ts new file mode 100644 index 0000000000..88832bbe3e --- /dev/null +++ b/packages/api/cli/spec/util/resolve-working-dir.spec.ts @@ -0,0 +1,49 @@ +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { resolveWorkingDir } from '../../src/util/resolve-working-dir'; + +describe('resolveWorkingDir', () => { + it('resolves relative paths according to the current working directory', () => { + const dir = resolveWorkingDir('.'); + + expect(dir).toEqual(process.cwd()); + }); + + it('works with an absolute directory', () => { + const upOne = path.resolve(process.cwd(), '..'); + expect(path.isAbsolute(upOne)).toBe(true); + const dir = resolveWorkingDir(upOne); + + expect(dir).toEqual(upOne); + }); + + it('resolves a relative path if checkExisting=false and dir does not exist', () => { + const dir = resolveWorkingDir('./i-made-this-dir-up', false); + + expect(dir).toEqual(path.resolve(process.cwd(), './i-made-this-dir-up')); + }); + + it('resolves an absolute path if checkExisting=false and dir does not exist', () => { + const fakeDir = path.resolve(process.cwd(), './i-made-this-dir-up'); + expect(path.isAbsolute(fakeDir)).toBe(true); + const dir = resolveWorkingDir(fakeDir, false); + + expect(dir).toEqual(fakeDir); + }); + + it('falls back to the current working directory with a relative path if checkExisting=true and dir does not exist', () => { + const dir = resolveWorkingDir('./i-made-this-dir-up', true); + + expect(dir).toEqual(process.cwd()); + }); + + it('falls back to the current working directory with an absolute path if checkExisting=true and dir does not exist', () => { + const fakeDir = path.resolve(process.cwd(), './i-made-this-dir-up'); + expect(path.isAbsolute(fakeDir)).toBe(true); + const dir = resolveWorkingDir(fakeDir); + + expect(dir).toEqual(process.cwd()); + }); +}); diff --git a/packages/api/cli/src/electron-forge-import.ts b/packages/api/cli/src/electron-forge-import.ts index 30a76fa803..30dbad42ad 100644 --- a/packages/api/cli/src/electron-forge-import.ts +++ b/packages/api/cli/src/electron-forge-import.ts @@ -4,21 +4,17 @@ import { program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; -import workingDir from './util/working-dir'; +import { resolveWorkingDir } from './util/resolve-working-dir'; -(async () => { - let dir = process.cwd(); - program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .helpOption('-h, --help', 'Output usage information') - .arguments('[name]') - .action((name) => { - dir = workingDir(dir, name, false); - }) - .parse(process.argv); - - await api.import({ - dir, - interactive: true, - }); -})(); +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)') + .action(async (dir: string) => { + const workingDir = resolveWorkingDir(dir, false); + await api.import({ + dir: workingDir, + interactive: true, + }); + }) + .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index 16c6077912..953388121c 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -4,31 +4,28 @@ import { program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; -import workingDir from './util/working-dir'; +import { resolveWorkingDir } from './util/resolve-working-dir'; -(async () => { - let dir = process.cwd(); - program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .arguments('[name]') - .option('-t, --template [name]', 'Name of the Forge template to use') - .option('-c, --copy-ci-files', 'Whether to copy the templated CI files', false) - .option('-f, --force', 'Whether to overwrite an existing directory', false) - .helpOption('-h, --help', 'Output usage information') - .action((name) => { - dir = workingDir(dir, name, false); - }) - .parse(process.argv); +program + .version(packageJSON.version, '-V, --version', 'Output the current version.') + .helpOption('-h, --help', 'Output usage information.') + .argument('[dir]', 'Directory to initialize the project in. (default: current directory)') + .option('-t, --template [name]', 'Name of the Forge template to use.', 'base') + .option('-c, --copy-ci-files', 'Whether to copy the templated CI files.', false) + .option('-f, --force', 'Whether to overwrite an existing directory.', false) + .action(async (dir) => { + const workingDir = resolveWorkingDir(dir, false); - const options = program.opts(); + const options = program.opts(); - const initOpts: InitOptions = { - dir, - interactive: true, - copyCIFiles: !!options.copyCiFiles, - force: !!options.force, - }; - if (options.template) initOpts.template = options.template; + const initOpts: InitOptions = { + dir: workingDir, + interactive: true, + copyCIFiles: !!options.copyCiFiles, + force: !!options.force, + }; + if (options.template) initOpts.template = options.template; - await api.init(initOpts); -})(); + await api.init(initOpts); + }) + .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge-make.ts b/packages/api/cli/src/electron-forge-make.ts index 2199e09d3f..34a24d5bb7 100644 --- a/packages/api/cli/src/electron-forge-make.ts +++ b/packages/api/cli/src/electron-forge-make.ts @@ -1,32 +1,33 @@ import { initializeProxy } from '@electron/get'; import { api, MakeOptions } from '@electron-forge/core'; +import chalk from 'chalk'; import { program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; -import workingDir from './util/working-dir'; +import { resolveWorkingDir } from './util/resolve-working-dir'; export async function getMakeOptions(): Promise { - let dir = process.cwd(); + let workingDir: string; program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .arguments('[cwd]') - .option('--skip-package', 'Assume the app is already packaged') - .option('-a, --arch [arch]', 'Target architecture') - .option('-p, --platform [platform]', 'Target build platform') - .option('--targets [targets]', 'Override your make targets for this run') - .helpOption('-h, --help', 'Output usage information') + .version(packageJSON.version, '-V, --version', 'Output the current version.') + .helpOption('-h, --help', 'Output usage information.') + .argument('[dir]', 'Directory to run the command in. (default: current directory)') + .option('--skip-package', `Skip packaging the Electron application, and use the output from a previous ${chalk.green('package')} run instead.`) + .option('-a, --arch [arch]', 'Target build architecture.', process.arch) + .option('-p, --platform [platform]', 'Target build platform.', process.platform) + .option('--targets [targets]', `Override your ${chalk.green('make')} targets for this run.`) .allowUnknownOption(true) - .action((cwd) => { - dir = workingDir(dir, cwd); + .action((dir) => { + workingDir = resolveWorkingDir(dir, false); }) .parse(process.argv); const options = program.opts(); const makeOpts: MakeOptions = { - dir, + dir: workingDir!, interactive: true, skipPackage: options.skipPackage, }; @@ -37,8 +38,7 @@ export async function getMakeOptions(): Promise { return makeOpts; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -if (require.main === module || (global as any).__LINKED_FORGE__) { +if (require.main === module) { (async () => { const makeOpts = await getMakeOptions(); diff --git a/packages/api/cli/src/electron-forge-package.ts b/packages/api/cli/src/electron-forge-package.ts index b082ff5979..a4b9d5e003 100644 --- a/packages/api/cli/src/electron-forge-package.ts +++ b/packages/api/cli/src/electron-forge-package.ts @@ -5,31 +5,28 @@ import { program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; -import workingDir from './util/working-dir'; +import { resolveWorkingDir } from './util/resolve-working-dir'; -(async () => { - let dir: string = process.cwd(); - program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .arguments('[cwd]') - .option('-a, --arch [arch]', 'Target architecture') - .option('-p, --platform [platform]', 'Target build platform') - .helpOption('-h, --help', 'Output usage information') - .action((cwd) => { - dir = workingDir(dir, cwd); - }) - .parse(process.argv); +program + .version(packageJSON.version, '-V, --version', 'Output the current version') + .helpOption('-h, --help', 'Output usage information') + .argument('[dir]', 'Directory to run the command in. (default: current directory)') + .option('-a, --arch [arch]', 'Target build architecture') + .option('-p, --platform [platform]', 'Target build platform') + .action(async (dir) => { + const workingDir = resolveWorkingDir(dir); - const options = program.opts(); + const options = program.opts(); - initializeProxy(); + initializeProxy(); - const packageOpts: PackageOptions = { - dir, - interactive: true, - }; - if (options.arch) packageOpts.arch = options.arch; - if (options.platform) packageOpts.platform = options.platform; + const packageOpts: PackageOptions = { + dir: workingDir, + interactive: true, + }; + if (options.arch) packageOpts.arch = options.arch; + if (options.platform) packageOpts.platform = options.platform; - await api.package(packageOpts); -})(); + await api.package(packageOpts); + }) + .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge-publish.ts b/packages/api/cli/src/electron-forge-publish.ts index 778e01b3b6..fddbc0fd10 100644 --- a/packages/api/cli/src/electron-forge-publish.ts +++ b/packages/api/cli/src/electron-forge-publish.ts @@ -1,41 +1,38 @@ import { initializeProxy } from '@electron/get'; import { api, PublishOptions } from '@electron-forge/core'; +import chalk from 'chalk'; import { program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; import { getMakeOptions } from './electron-forge-make'; -import workingDir from './util/working-dir'; - -(async () => { - let dir = process.cwd(); - program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .arguments('[cwd]') - .option('--target [target[,target...]]', 'The comma-separated deployment targets, defaults to "github"') - .option('--dry-run', "Triggers a publish dry run which saves state and doesn't upload anything") - .option('--from-dry-run', 'Attempts to publish artifacts from the last saved dry run') - .helpOption('-h, --help', 'Output usage information') - .allowUnknownOption(true) - .action((cwd) => { - dir = workingDir(dir, cwd); - }) - .parse(process.argv); - - const options = program.opts(); - - initializeProxy(); - - const publishOpts: PublishOptions = { - dir, - interactive: true, - dryRun: options.dryRun, - dryRunResume: options.fromDryRun, - }; - if (options.target) publishOpts.publishTargets = options.target.split(','); - - publishOpts.makeOptions = await getMakeOptions(); - - await api.publish(publishOpts); -})(); +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 to run the command in. (default: current directory)') + .option('--target [target[,target...]]', 'A comma-separated list of deployment targets. (default: all publishers in your Forge config)') + .option('--dry-run', `Run the ${chalk.green('make')} command and save publish metadata without uploading anything.`) + .option('--from-dry-run', 'Publish artifacts from the last saved dry run.') + .allowUnknownOption(true) + .action(async (targetDir) => { + const dir = resolveWorkingDir(targetDir); + const options = program.opts(); + + initializeProxy(); + + const publishOpts: PublishOptions = { + dir, + interactive: true, + dryRun: options.dryRun, + dryRunResume: options.fromDryRun, + }; + if (options.target) publishOpts.publishTargets = options.target.split(','); + + publishOpts.makeOptions = await getMakeOptions(); + + await api.publish(publishOpts); + }) + .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge-start.ts b/packages/api/cli/src/electron-forge-start.ts index 510f409923..42c490904b 100644 --- a/packages/api/cli/src/electron-forge-start.ts +++ b/packages/api/cli/src/electron-forge-start.ts @@ -1,11 +1,11 @@ import { api, StartOptions } from '@electron-forge/core'; import { ElectronProcess } from '@electron-forge/shared-types'; -import { program } from 'commander'; +import { Option, program } from 'commander'; import './util/terminate'; import packageJSON from '../package.json'; -import workingDir from './util/working-dir'; +import { resolveWorkingDir } from './util/resolve-working-dir'; (async () => { let commandArgs = process.argv; @@ -17,16 +17,17 @@ import workingDir from './util/working-dir'; appArgs = process.argv.slice(doubleDashIndex + 1); } - let dir = process.cwd(); + let dir; program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .arguments('[cwd]') - .option('-p, --app-path ', "Override the path to the Electron app to launch (defaults to '.')") - .option('-l, --enable-logging', 'Enable advanced logging. This will log internal Electron things') - .option('-n, --run-as-node', 'Run the Electron app as a Node.JS script') - .option('--vscode', 'Used to enable arg transformation for debugging Electron through VSCode. Do not use yourself.') - .option('-i, --inspect-electron', 'Triggers inspect mode on Electron to allow debugging the main process. Electron >1.7 only') - .option('--inspect-brk-electron', 'Triggers inspect-brk mode on Electron to allow debugging the main process. Electron >1.7 only') + .version(packageJSON.version, '-V, --version', 'Output the current version.') + .helpOption('-h, --help', 'Output usage information.') + .argument('[dir]', 'Directory to run the command in. (default: current directory)') + .option('-p, --app-path ', 'Path to the Electron app to launch. (default: current directory)') + .option('-l, --enable-logging', 'Enable internal Electron logging.') + .option('-n, --run-as-node', 'Run the Electron app as a Node.JS script.') + .addOption(new Option('--vscode').hideHelp()) // Used to enable arg transformation for debugging Electron through VSCode. Hidden from users. + .option('-i, --inspect-electron', 'Run Electron in inspect mode to allow debugging the main process.') + .option('--inspect-brk-electron', 'Run Electron in inspect-brk mode to allow debugging the main process.') .addHelpText( 'after', ` @@ -37,8 +38,8 @@ import workingDir from './util/working-dir'; ...will pass the arguments "-d -f foo.txt" to the Electron app.` ) .passThroughOptions(true) // allows args to be passed down to the Electron executable - .action((cwd) => { - dir = workingDir(dir, cwd); + .action((targetDir: string) => { + dir = resolveWorkingDir(targetDir); }) .parse(commandArgs); diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 069619e64e..bfc3f79991 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -12,15 +12,14 @@ import packageJSON from '../package.json'; import { checkSystem } from './util/check-system'; program - .version(packageJSON.version, '-V, --version', 'Output the current version') - .option('--verbose', 'Enables verbose mode') - .helpOption('-h, --help', 'Output usage information') - .command('init', 'Initialize a new Electron application') - .command('import', 'Attempts to navigate you through the process of importing an existing project to "electron-forge"') - .command('start', 'Start the current Electron application in development mode') - .command('package', 'Package the current Electron application') - .command('make', 'Generate distributables for the current Electron application') - .command('publish', 'Publish the current Electron application') + .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.') + .command('package', 'Package the current Electron application.') + .command('make', 'Generate distributables for the current Electron application.') + .command('publish', 'Publish the current Electron application.') .hook('preSubcommand', async (_command, subcommand) => { if (!process.argv.includes('--help') && !process.argv.includes('-h')) { const runner = new Listr<{ diff --git a/packages/api/cli/src/util/resolve-working-dir.ts b/packages/api/cli/src/util/resolve-working-dir.ts new file mode 100644 index 0000000000..e279b7a6a2 --- /dev/null +++ b/packages/api/cli/src/util/resolve-working-dir.ts @@ -0,0 +1,23 @@ +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/api/cli/src/util/working-dir.ts b/packages/api/cli/src/util/working-dir.ts deleted file mode 100644 index 34f7db3df7..0000000000 --- a/packages/api/cli/src/util/working-dir.ts +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'node:path'; - -import fs from 'fs-extra'; - -export default function workingDir(dir: string, cwd: string, checkExisting = true): string { - let finalDir = dir; - if (cwd) { - if (path.isAbsolute(cwd) && (!checkExisting || fs.existsSync(cwd))) { - finalDir = cwd; - } else { - const resolved = path.resolve(finalDir, cwd); - if (!checkExisting || fs.existsSync(resolved)) { - finalDir = resolved; - } - } - } - - return finalDir; -}