diff --git a/apps/cli/ai/providers.ts b/apps/cli/ai/providers.ts index 35c72514ea..d572edf8e1 100644 --- a/apps/cli/ai/providers.ts +++ b/apps/cli/ai/providers.ts @@ -1,4 +1,3 @@ -import { password } from '@inquirer/prompts'; import { validateAnthropicApiKey } from '@studio/common/ai/anthropic-key'; import { DEFAULT_MODEL, type AiModelId } from '@studio/common/ai/models'; import { @@ -10,6 +9,7 @@ import { import { persistAnthropicApiKey, readAnthropicApiKey } from '@studio/common/ai/settings-store'; import { readAuthToken } from '@studio/common/lib/shared-config'; import { __ } from '@wordpress/i18n'; +import { password } from 'cli/lib/prompts'; import { LoggerError } from 'cli/logger'; export const AI_PROVIDERS: Record< AiProviderId, string > = { diff --git a/apps/cli/commands/ai/sessions/helpers.ts b/apps/cli/commands/ai/sessions/helpers.ts index bafb3a0c44..0aa96b6e5d 100644 --- a/apps/cli/commands/ai/sessions/helpers.ts +++ b/apps/cli/commands/ai/sessions/helpers.ts @@ -1,9 +1,9 @@ import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; -import { select } from '@inquirer/prompts'; import { listAiSessions } from '@studio/common/ai/sessions/store'; import chalk from '@studio/common/lib/chalk'; import { getSessionsDirectory } from '@studio/common/lib/well-known-paths'; import { __ } from '@wordpress/i18n'; +import { select } from 'cli/lib/prompts'; import type { AiSessionSummary } from '@studio/common/ai/sessions/types'; function formatSessionTimestamp( timestamp: string ): string { diff --git a/apps/cli/commands/auth/login.ts b/apps/cli/commands/auth/login.ts index a06ff1694d..33c48f31c3 100644 --- a/apps/cli/commands/auth/login.ts +++ b/apps/cli/commands/auth/login.ts @@ -1,8 +1,8 @@ -import { input } from '@inquirer/prompts'; import { readAuthToken } from '@studio/common/lib/shared-config'; import { AuthCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions'; import { __ } from '@wordpress/i18n'; import { openBrowser } from 'cli/lib/browser'; +import { input } from 'cli/lib/prompts'; import { getCliAuthenticationUrl, storeAuthToken } from 'cli/lib/wpcom-auth'; import { Logger, LoggerError } from 'cli/logger'; import { StudioArgv } from 'cli/types'; diff --git a/apps/cli/commands/blueprint/use.ts b/apps/cli/commands/blueprint/use.ts index 107cf1fb68..005e4e8898 100644 --- a/apps/cli/commands/blueprint/use.ts +++ b/apps/cli/commands/blueprint/use.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import path from 'path'; -import { select, input } from '@inquirer/prompts'; import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from '@studio/common/constants'; import { createBlueprintTempDir, @@ -15,6 +14,7 @@ import { BlueprintCommandLoggerAction as LoggerAction } from '@studio/common/log import { SupportedPHPVersions, type SupportedPHPVersion } from '@studio/common/types/php-versions'; import { __, _n, sprintf } from '@wordpress/i18n'; import { runCommand as runCreateSiteCommand } from 'cli/commands/site/create'; +import { select, input } from 'cli/lib/prompts'; import { getDefaultSitePath } from 'cli/lib/site-paths'; import { untildify } from 'cli/lib/utils'; import { diff --git a/apps/cli/commands/pull.ts b/apps/cli/commands/pull.ts index 1ec1298270..777ca4c47f 100644 --- a/apps/cli/commands/pull.ts +++ b/apps/cli/commands/pull.ts @@ -1,7 +1,6 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { confirm } from '@inquirer/prompts'; import { addConnectedWpcomSite, markConnectedWpcomSiteSynced, @@ -20,6 +19,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { clearSiteLatestCliPid, getSiteByFolder } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { DEFAULT_IMPORTER_OPTIONS, getImporter } from 'cli/lib/import-export/import/import-manager'; +import { confirm } from 'cli/lib/prompts'; import { withSiteOperation } from 'cli/lib/site-operations'; import { exitOnCancel, reportSyncActivity } from 'cli/lib/sync-activity'; import { diff --git a/apps/cli/commands/site/create.ts b/apps/cli/commands/site/create.ts index e99a065a4b..efff860ac9 100644 --- a/apps/cli/commands/site/create.ts +++ b/apps/cli/commands/site/create.ts @@ -1,7 +1,6 @@ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; -import { confirm, input, password, select } from '@inquirer/prompts'; import { DEFAULT_WORDPRESS_VERSION } from '@studio/common/constants'; import { installAiInstructionsToSite } from '@studio/common/lib/agent-skills'; import { @@ -69,6 +68,7 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup'; import { downloadWordPress } from 'cli/lib/dependency-management/wordpress'; import { copyLanguagePackToSite } from 'cli/lib/language-packs'; import { validateSupportedPhpVersion } from 'cli/lib/php-versions'; +import { confirm, input, password, select } from 'cli/lib/prompts'; import { getPreferredSiteLanguage } from 'cli/lib/site-language'; import { generateSiteName } from 'cli/lib/site-name'; import { getDefaultSitePath } from 'cli/lib/site-paths'; diff --git a/apps/cli/commands/uninstall.ts b/apps/cli/commands/uninstall.ts index 98ad40472a..ba8152e329 100644 --- a/apps/cli/commands/uninstall.ts +++ b/apps/cli/commands/uninstall.ts @@ -2,11 +2,11 @@ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { confirm } from '@inquirer/prompts'; import { getConfigDirectory } from '@studio/common/lib/well-known-paths'; import { __, sprintf } from '@wordpress/i18n'; import trash from 'trash'; import { Mode, runCommand as stopSites } from 'cli/commands/site/stop'; +import { confirm } from 'cli/lib/prompts'; import { getCliInstallKind } from 'cli/lib/update-notifier'; import { StudioArgv } from 'cli/types'; diff --git a/apps/cli/lib/prompts.ts b/apps/cli/lib/prompts.ts new file mode 100644 index 0000000000..ea63e025b0 --- /dev/null +++ b/apps/cli/lib/prompts.ts @@ -0,0 +1,33 @@ +import type * as Prompts from '@inquirer/prompts'; + +// `@inquirer/prompts` loads every prompt type, each with its own copy of `@inquirer/core`, +// so these load it when a prompt is shown instead of when `studio` starts. + +export async function confirm( ...args: Parameters< typeof Prompts.confirm > ) { + const { confirm: prompt } = await import( '@inquirer/prompts' ); + return prompt( ...args ); +} + +export async function input( ...args: Parameters< typeof Prompts.input > ) { + const { input: prompt } = await import( '@inquirer/prompts' ); + return prompt( ...args ); +} + +export async function password( ...args: Parameters< typeof Prompts.password > ) { + const { password: prompt } = await import( '@inquirer/prompts' ); + return prompt( ...args ); +} + +export async function select< const Value >( + ...args: Parameters< typeof Prompts.select< Value > > +): Promise< Value > { + const { select: prompt } = await import( '@inquirer/prompts' ); + return prompt< Value >( ...args ); +} + +export async function search< const Value >( + ...args: Parameters< typeof Prompts.search< Value > > +): Promise< Value > { + const { search: prompt } = await import( '@inquirer/prompts' ); + return prompt< Value >( ...args ); +} diff --git a/apps/cli/lib/sync-site-picker.ts b/apps/cli/lib/sync-site-picker.ts index f5a769f2de..8647ef132f 100644 --- a/apps/cli/lib/sync-site-picker.ts +++ b/apps/cli/lib/sync-site-picker.ts @@ -1,6 +1,6 @@ -import { search } from '@inquirer/prompts'; import chalk from '@studio/common/lib/chalk'; import { __, sprintf } from '@wordpress/i18n'; +import { search } from 'cli/lib/prompts'; import { normalizeHostname } from 'cli/lib/utils'; import { LoggerError } from 'cli/logger'; import type { SyncSite } from '@studio/common/types/sync'; diff --git a/apps/cli/migrations/00-check-studio-compatibility.ts b/apps/cli/migrations/00-check-studio-compatibility.ts index 16c770abc0..0de386e8f9 100644 --- a/apps/cli/migrations/00-check-studio-compatibility.ts +++ b/apps/cli/migrations/00-check-studio-compatibility.ts @@ -1,10 +1,10 @@ import { execFile } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { confirm } from '@inquirer/prompts'; import { getAppConfigPath } from '@studio/common/lib/well-known-paths'; import { __ } from '@wordpress/i18n'; import { getAppdataDirectory } from 'cli/lib/appdata'; +import { confirm } from 'cli/lib/prompts'; import type { Migration } from '@studio/common/lib/migration'; function isInstalledOnMacOs() { diff --git a/apps/cli/tests/startup-imports.test.ts b/apps/cli/tests/startup-imports.test.ts index e3d06f5b99..13b02b468d 100644 --- a/apps/cli/tests/startup-imports.test.ts +++ b/apps/cli/tests/startup-imports.test.ts @@ -2,25 +2,31 @@ import { readFileSync } from 'fs'; import path from 'path'; import { describe, expect, it, vi } from 'vitest'; -const agentRuntimeLoaded = vi.hoisted( () => ( { value: false } ) ); -const dateFnsRootLoaded = vi.hoisted( () => ( { value: false } ) ); +const loadedAtStartup = vi.hoisted( () => new Set< string >() ); +// Modules that each cost hundreds of milliseconds to load in the packaged app, so only the +// code that uses them loads them. +// The agent runtime is large. vi.mock( '@earendil-works/pi-coding-agent', () => { - agentRuntimeLoaded.value = true; + loadedAtStartup.add( '@earendil-works/pi-coding-agent' ); return {}; } ); - -// The `date-fns` root loads every one of its functions, about 300 modules, which took -// about 0.9 s in the packaged app. Each function has its own path, e.g. `date-fns/format`. +// The `date-fns` root loads every one of its functions, about 300 modules. Each function +// has its own path, e.g. `date-fns/format`. vi.mock( 'date-fns', () => { - dateFnsRootLoaded.value = true; + loadedAtStartup.add( 'date-fns' ); + return {}; +} ); +// `@inquirer/prompts` loads every prompt type, each with its own copy of `@inquirer/core`. +// `cli/lib/prompts` loads it when a prompt is shown. +vi.mock( '@inquirer/prompts', () => { + loadedAtStartup.add( '@inquirer/prompts' ); return {}; } ); -// Every `studio` command loads what the CLI entry imports statically before it -// runs. The agent runtime is large, so only the commands that use it load it. +// Every `studio` command loads what the CLI entry imports statically before it runs. describe( 'CLI startup', () => { - it( 'loads no agent runtime or date-fns root before a command runs', async () => { + it( 'loads no heavy module before a command runs', async () => { const entry = readFileSync( path.join( import.meta.dirname, '..', 'index.ts' ), 'utf8' ); const staticImports = [ ...entry.matchAll( /^import [^;]*? from '(cli\/[^']+)';$/gm ) ].map( ( [ , specifier ] ) => specifier @@ -31,7 +37,6 @@ describe( 'CLI startup', () => { await import( /* @vite-ignore */ specifier ); } - expect( agentRuntimeLoaded.value ).toBe( false ); - expect( dateFnsRootLoaded.value ).toBe( false ); + expect( [ ...loadedAtStartup ] ).toEqual( [] ); } ); } );