diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index ee3c066c6d..5d66190187 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -25,6 +25,7 @@ import {getCachedAppInfo, setCachedAppInfo} from './local-storage.js' import {canEnablePreviewMode} from './extensions/common.js' import {fetchAppRemoteConfiguration} from './app/select-app.js' import {patchAppConfigurationFile} from './app/patch-app-configuration-file.js' +import {DevSessionStatusManager} from './dev/processes/dev-session-status-manager.js' import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' import {Web, isCurrentAppSchema, getAppScopesArray, AppLinkedInterface} from '../models/app/app.js' import {Organization, OrganizationApp, OrganizationStore} from '../models/organization.js' @@ -71,9 +72,9 @@ export interface DevOptions { export async function dev(commandOptions: DevOptions) { const config = await prepareForDev(commandOptions) await actionsBeforeSettingUpDevProcesses(config) - const {processes, graphiqlUrl, previewUrl} = await setupDevProcesses(config) + const {processes, graphiqlUrl, previewUrl, devSessionStatusManager} = await setupDevProcesses(config) await actionsBeforeLaunchingDevProcesses(config) - await launchDevProcesses({processes, previewUrl, graphiqlUrl, config}) + await launchDevProcesses({processes, previewUrl, graphiqlUrl, config, devSessionStatusManager}) } async function prepareForDev(commandOptions: DevOptions): Promise { @@ -117,10 +118,10 @@ async function prepareForDev(commandOptions: DevOptions): Promise { await installAppDependencies(app) } - const graphiqlPort = commandOptions.graphiqlPort || (await getAvailableTCPPort(ports.graphiql)) + const graphiqlPort = commandOptions.graphiqlPort ?? (await getAvailableTCPPort(ports.graphiql)) const {graphiqlKey} = commandOptions - if (graphiqlPort !== (commandOptions.graphiqlPort || ports.graphiql)) { + if (graphiqlPort !== (commandOptions.graphiqlPort ?? ports.graphiql)) { renderWarning({ headline: [ 'A random port will be used for GraphiQL because', @@ -224,7 +225,7 @@ export async function warnIfScopesDifferBeforeDev({ scopesMessage(getAppScopesArray(localApp.configuration)), '\n', 'Scopes in Partner Dashboard:', - scopesMessage(remoteAccess?.scopes?.split(',') || []), + scopesMessage(remoteAccess?.scopes?.split(',') ?? []), ], nextSteps, }) @@ -301,7 +302,7 @@ async function setupNetworkingOptions( ...frontEndOptions, tunnelClient, }), - getBackendPort() || backendConfig?.configuration.port || getAvailableTCPPort(), + getBackendPort() ?? backendConfig?.configuration.port ?? getAvailableTCPPort(), getURLs(remoteAppConfig), ]) const proxyUrl = usingLocalhost ? `${frontendUrl}:${proxyPort}` : frontendUrl @@ -330,11 +331,13 @@ async function launchDevProcesses({ previewUrl, graphiqlUrl, config, + devSessionStatusManager, }: { processes: DevProcesses previewUrl: string graphiqlUrl: string | undefined config: DevConfig + devSessionStatusManager: DevSessionStatusManager }) { const abortController = new AbortController() const processesForTaskRunner: OutputProcess[] = processes.map((process) => { @@ -375,6 +378,7 @@ async function launchDevProcesses({ abortController, developerPreview: developerPreviewController(apiKey, developerPlatformClient), shopFqdn: config.storeFqdn, + devSessionStatusManager, }) } diff --git a/packages/app/src/cli/services/dev/processes/dev-session-status-manager.test.ts b/packages/app/src/cli/services/dev/processes/dev-session-status-manager.test.ts index b754210a6e..b74d8bc825 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session-status-manager.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session-status-manager.test.ts @@ -1,7 +1,9 @@ -import {devSessionStatusManager, DevSessionStatus} from './dev-session-status-manager.js' +import {DevSessionStatus, DevSessionStatusManager} from './dev-session-status-manager.js' import {describe, test, expect, beforeEach, vi} from 'vitest' describe('DevSessionStatusManager', () => { + const devSessionStatusManager = new DevSessionStatusManager() + beforeEach(() => { devSessionStatusManager.removeAllListeners() devSessionStatusManager.reset() diff --git a/packages/app/src/cli/services/dev/processes/dev-session-status-manager.ts b/packages/app/src/cli/services/dev/processes/dev-session-status-manager.ts index b4feacb146..42360397f5 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session-status-manager.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session-status-manager.ts @@ -14,6 +14,11 @@ export class DevSessionStatusManager extends EventEmitter { graphiqlURL: undefined, } + constructor(defaultStatus?: DevSessionStatus) { + super() + if (defaultStatus) this.currentStatus = defaultStatus + } + updateStatus(status: Partial) { const newStatus = {...this.currentStatus, ...status} // Only emit if status has changed @@ -35,5 +40,3 @@ export class DevSessionStatusManager extends EventEmitter { } } } - -export const devSessionStatusManager = new DevSessionStatusManager() diff --git a/packages/app/src/cli/services/dev/processes/dev-session.test.ts b/packages/app/src/cli/services/dev/processes/dev-session.test.ts index 1eaaa421f2..3a8b534662 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session.test.ts @@ -1,5 +1,5 @@ import {setupDevSessionProcess, pushUpdatesForDevSession} from './dev-session.js' -import {devSessionStatusManager} from './dev-session-status-manager.js' +import {DevSessionStatusManager} from './dev-session-status-manager.js' import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' import {AppLinkedInterface} from '../../../models/app/app.js' import {AppEventWatcher} from '../app-events/app-event-watcher.js' @@ -39,6 +39,7 @@ describe('setupDevSessionProcess', () => { appWatcher: {} as AppEventWatcher, appPreviewURL: 'https://test.preview.url', appLocalProxyURL: 'https://test.local.url', + devSessionStatusManager: new DevSessionStatusManager(), } // When @@ -60,6 +61,7 @@ describe('setupDevSessionProcess', () => { appWatcher: options.appWatcher, appPreviewURL: options.appPreviewURL, appLocalProxyURL: options.appLocalProxyURL, + devSessionStatusManager: options.devSessionStatusManager, }, }) }) @@ -73,6 +75,7 @@ describe('pushUpdatesForDevSession', () => { let appWatcher: AppEventWatcher let app: AppLinkedInterface let abortController: AbortController + let devSessionStatusManager: DevSessionStatusManager beforeEach(() => { vi.mocked(formData).mockReturnValue({append: vi.fn(), getHeaders: vi.fn()} as any) @@ -83,7 +86,7 @@ describe('pushUpdatesForDevSession', () => { app = testAppLinked() appWatcher = new AppEventWatcher(app) abortController = new AbortController() - devSessionStatusManager.reset() + devSessionStatusManager = new DevSessionStatusManager() options = { developerPlatformClient, appWatcher, @@ -92,6 +95,7 @@ describe('pushUpdatesForDevSession', () => { organizationId: 'org123', appPreviewURL: 'https://test.preview.url', appLocalProxyURL: 'https://test.local.url', + devSessionStatusManager, } }) diff --git a/packages/app/src/cli/services/dev/processes/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session.ts index 7230dd482f..6bc6532ebd 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session.ts @@ -1,5 +1,5 @@ import {BaseProcess, DevProcessFunction} from './types.js' -import {devSessionStatusManager} from './dev-session-status-manager.js' +import {DevSessionStatusManager} from './dev-session-status-manager.js' import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' import {AppLinkedInterface} from '../../../models/app/app.js' import {getExtensionUploadURL} from '../../deploy/upload.js' @@ -31,6 +31,7 @@ interface DevSessionOptions { appWatcher: AppEventWatcher appPreviewURL: string appLocalProxyURL: string + devSessionStatusManager: DevSessionStatusManager } interface DevSessionProcessOptions extends DevSessionOptions { @@ -88,7 +89,7 @@ export const pushUpdatesForDevSession: DevProcessFunction = a {stderr, stdout, abortSignal: signal}, options, ) => { - const {appWatcher} = options + const {appWatcher, devSessionStatusManager} = options const processOptions = {...options, stderr, stdout, signal, bundlePath: appWatcher.buildOutputPath} @@ -164,7 +165,7 @@ async function handleDevSessionResult( await printSuccess(`✅ Updated`, processOptions.stdout) await printActionRequiredMessages(processOptions, event) } else if (result.status === 'created') { - devSessionStatusManager.updateStatus({isReady: true}) + processOptions.devSessionStatusManager.updateStatus({isReady: true}) await printSuccess(`✅ Ready, watching for changes in your app `, processOptions.stdout) } else if (result.status === 'aborted') { outputDebug('❌ Session update aborted (new change detected or error in Session Update)', processOptions.stdout) @@ -174,7 +175,7 @@ async function handleDevSessionResult( // If we failed to create a session, exit the process. Don't throw an error in tests as it can't be caught due to the // async nature of the process. - if (!devSessionStatusManager.status.isReady && !isUnitTest()) { + if (!processOptions.devSessionStatusManager.status.isReady && !isUnitTest()) { throw new AbortError('Failed to start dev session.') } } @@ -253,7 +254,7 @@ async function bundleExtensionsAndUpload(options: DevSessionProcessOptions): Pro // Create or update the dev session if (currentBundleController.signal.aborted) return {status: 'aborted'} try { - if (devSessionStatusManager.status.isReady) { + if (options.devSessionStatusManager.status.isReady) { const result = await devSessionUpdateWithRetry(payload, options.developerPlatformClient) const errors = result.devSessionUpdate?.userErrors ?? [] if (errors.length) return {status: 'remote-error', error: errors} @@ -363,5 +364,5 @@ async function printLogMessage(message: string, stdout: Writable, prefix?: strin async function updatePreviewURL(options: DevSessionProcessOptions, event: AppEvent) { const hasPreview = event.app.allExtensions.filter((ext) => ext.isPreviewable).length > 0 const newPreviewURL = hasPreview ? options.appLocalProxyURL : options.appPreviewURL - devSessionStatusManager.updateStatus({previewURL: newPreviewURL}) + options.devSessionStatusManager.updateStatus({previewURL: newPreviewURL}) } diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index 75ed92d33f..64e597f2b5 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -8,7 +8,7 @@ import {WebProcess, setupWebProcesses} from './web.js' import {DevSessionProcess, setupDevSessionProcess} from './dev-session.js' import {AppLogsSubscribeProcess, setupAppLogsPollingProcess} from './app-logs-polling.js' import {AppWatcherProcess, setupAppWatcherProcess} from './app-watcher-process.js' -import {devSessionStatusManager} from './dev-session-status-manager.js' +import {DevSessionStatusManager} from './dev-session-status-manager.js' import {environmentVariableNames} from '../../../constants.js' import {AppLinkedInterface, getAppScopes, WebType} from '../../../models/app/app.js' @@ -79,6 +79,7 @@ export async function setupDevProcesses({ processes: DevProcesses previewUrl: string graphiqlUrl: string | undefined + devSessionStatusManager: DevSessionStatusManager }> { const apiKey = remoteApp.apiKey const apiSecret = remoteApp.apiSecretKeys[0]?.secret ?? '' @@ -100,7 +101,7 @@ export async function setupDevProcesses({ ? `http://localhost:${graphiqlPort}/graphiql${graphiqlKey ? `?key=${graphiqlKey}` : ''}` : undefined - devSessionStatusManager.updateStatus({isReady: false, previewURL, graphiqlURL}) + const devSessionStatusManager = new DevSessionStatusManager({isReady: false, previewURL, graphiqlURL}) const processes = [ ...(await setupWebProcesses({ @@ -150,6 +151,7 @@ export async function setupDevProcesses({ appWatcher, appPreviewURL: appPreviewUrl, appLocalProxyURL: devConsoleURL, + devSessionStatusManager, }) : await setupDraftableExtensionsProcess({ localApp: reloadedApp, @@ -198,6 +200,7 @@ export async function setupDevProcesses({ processes: processesWithProxy, previewUrl: previewURL, graphiqlUrl: graphiqlURL, + devSessionStatusManager, } } diff --git a/packages/app/src/cli/services/dev/ui.test.tsx b/packages/app/src/cli/services/dev/ui.test.tsx index c9576b10f8..b2096ab83b 100644 --- a/packages/app/src/cli/services/dev/ui.test.tsx +++ b/packages/app/src/cli/services/dev/ui.test.tsx @@ -1,7 +1,7 @@ import {renderDev} from './ui.js' import {Dev} from './ui/components/Dev.js' import {DevSessionUI} from './ui/components/DevSessionUI.js' -import {devSessionStatusManager} from './processes/dev-session-status-manager.js' +import {DevSessionStatusManager} from './processes/dev-session-status-manager.js' import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' import {afterEach, describe, expect, test, vi} from 'vitest' import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' @@ -21,6 +21,7 @@ const developerPreview = { } const developerPlatformClient = testDeveloperPlatformClient() +const devSessionStatusManager = new DevSessionStatusManager() afterEach(() => { mockAndCaptureOutput().clear() @@ -60,6 +61,7 @@ describe('ui', () => { abortController, developerPreview, shopFqdn, + devSessionStatusManager, }) expect(vi.mocked(Dev)).not.toHaveBeenCalled() @@ -103,6 +105,7 @@ describe('ui', () => { abortController, developerPreview, shopFqdn, + devSessionStatusManager, }) abortController.abort() @@ -142,6 +145,7 @@ describe('ui', () => { abortController, developerPreview, shopFqdn, + devSessionStatusManager, }) abortController.abort() @@ -173,7 +177,17 @@ describe('ui', () => { const abortController = new AbortController() // eslint-disable-next-line @typescript-eslint/no-floating-promises - renderDev({processes, previewUrl, graphiqlUrl, graphiqlPort, app, abortController, developerPreview, shopFqdn}) + renderDev({ + processes, + previewUrl, + graphiqlUrl, + graphiqlPort, + app, + abortController, + developerPreview, + shopFqdn, + devSessionStatusManager, + }) await new Promise((resolve) => setTimeout(resolve, 100)) @@ -209,7 +223,17 @@ describe('ui', () => { const abortController = new AbortController() // eslint-disable-next-line @typescript-eslint/no-floating-promises - renderDev({processes, previewUrl, graphiqlUrl, graphiqlPort, app, abortController, developerPreview, shopFqdn}) + renderDev({ + processes, + previewUrl, + graphiqlUrl, + graphiqlPort, + app, + abortController, + developerPreview, + shopFqdn, + devSessionStatusManager, + }) await new Promise((resolve) => setTimeout(resolve, 100)) @@ -258,6 +282,7 @@ describe('ui', () => { abortController, developerPreview, shopFqdn, + devSessionStatusManager, }) await new Promise((resolve) => setTimeout(resolve, 100)) diff --git a/packages/app/src/cli/services/dev/ui.tsx b/packages/app/src/cli/services/dev/ui.tsx index 13a03806e7..6fb6f2fcfc 100644 --- a/packages/app/src/cli/services/dev/ui.tsx +++ b/packages/app/src/cli/services/dev/ui.tsx @@ -1,6 +1,6 @@ import {Dev, DevProps} from './ui/components/Dev.js' import {DevSessionUI} from './ui/components/DevSessionUI.js' -import {devSessionStatusManager} from './processes/dev-session-status-manager.js' +import {DevSessionStatusManager} from './processes/dev-session-status-manager.js' import React from 'react' import {render} from '@shopify/cli-kit/node/ui' import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' @@ -16,7 +16,8 @@ export async function renderDev({ graphiqlPort, developerPreview, shopFqdn, -}: DevProps) { + devSessionStatusManager, +}: DevProps & {devSessionStatusManager: DevSessionStatusManager}) { if (!terminalSupportsPrompting()) { await renderDevNonInteractive({processes, app, abortController, developerPreview, shopFqdn}) } else if (app.developerPlatformClient.supportsDevSessions) {