From 66b41fd6ce935580c2566567a78bad2356bba69d Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:08:45 -0400 Subject: [PATCH 1/3] Add Dev Console shortcut for non-embedded apps in app dev For non-embedded apps, adds a (c) keyboard shortcut and URL link in the DevSessionUI footer that opens the store admin with ?dev-console=show, giving developers quick access to extension previews. Also fixes AppManagementClient.appFromIdentifiers() to extract the embedded field from the app home module config, and makes the embedded state reactive via DevSessionStatusManager so it updates live when the toml changes during dev. Co-Authored-By: Claude Opus 4.6 --- .changeset/blue-taxes-cry.md | 5 ++ .../dev-session/dev-session-status-manager.ts | 2 + .../dev/processes/dev-session/dev-session.ts | 7 +- .../dev/processes/setup-dev-processes.ts | 10 ++- .../dev/ui/components/DevSessionUI.test.tsx | 86 ++++++++++++++++++- .../dev/ui/components/DevSessionUI.tsx | 29 ++++++- packages/app/src/cli/utilities/app/app-url.ts | 4 + .../app-management-client.ts | 1 + 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 .changeset/blue-taxes-cry.md diff --git a/.changeset/blue-taxes-cry.md b/.changeset/blue-taxes-cry.md new file mode 100644 index 00000000000..1da22306571 --- /dev/null +++ b/.changeset/blue-taxes-cry.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Added a separate Dev Console link to the `app dev` output for non-embedded apps diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts index 027caff0bcf..7d41b0d3f7d 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts @@ -17,6 +17,8 @@ export interface DevSessionStatus { isReady: boolean previewURL?: string graphiqlURL?: string + appEmbedded?: boolean + hasExtensions?: boolean statusMessage?: {message: string; type: DevSessionStatusMessageType} } diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts index b6aab4c6813..f1747345f52 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts @@ -269,7 +269,12 @@ export class DevSession { const hasPreview = event.app.allExtensions.filter((ext) => ext.isPreviewable).length > 0 const useDevConsole = firstPartyDev() && hasPreview const newPreviewURL = useDevConsole ? this.options.appLocalProxyURL : this.options.appPreviewURL - this.statusManager.updateStatus({previewURL: newPreviewURL}) + const hasExtensions = event.app.allExtensions.length > 0 + this.statusManager.updateStatus({ + previewURL: newPreviewURL, + appEmbedded: event.app.configuration.embedded, + hasExtensions, + }) } /** 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 a7ece06e3dc..b7e3390aaa1 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 @@ -125,7 +125,15 @@ export async function setupDevProcesses({ ? `http://localhost:${graphiqlPort}/graphiql?key=${encodeURIComponent(resolvedGraphiqlKey)}` : undefined - const devSessionStatusManager = new DevSessionStatusManager({isReady: false, previewURL, graphiqlURL}) + const appEmbedded = reloadedApp.configuration.embedded + const hasExtensions = reloadedApp.allExtensions.length > 0 + const devSessionStatusManager = new DevSessionStatusManager({ + isReady: false, + previewURL, + graphiqlURL, + appEmbedded, + hasExtensions, + }) const processes = [ ...(await setupWebProcesses({ diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx index ac93a0479fc..7d6b5f5ab61 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx @@ -40,6 +40,8 @@ const initialStatus: DevSessionStatus = { isReady: true, previewURL: 'https://shopify.com', graphiqlURL: 'https://graphiql.shopify.com', + appEmbedded: false, + hasExtensions: true, } const onAbort = vi.fn() @@ -121,10 +123,12 @@ describe('DevSessionUI', () => { expect(output).toContain('(q) Quit') // Shortcuts and URLs should be visible - expect(output).toContain('(g) Open GraphiQL') - expect(output).toContain('(p) Preview in your browser') + expect(output).toContain('(g) Open GraphiQL (Admin API)') + expect(output).toContain('(p) Open app preview') + expect(output).toContain('(c) Open Dev Console for extension previews') expect(output).toContain('Preview URL: https://shopify.com') expect(output).toContain('GraphiQL URL: https://graphiql.shopify.com') + expect(output).toContain('Dev Console URL: https://mystore.myshopify.com/admin?dev-console=show') renderInstance.unmount() }) @@ -171,6 +175,80 @@ describe('DevSessionUI', () => { renderInstance.unmount() }) + test('opens the dev console URL when c is pressed for non-embedded apps', async () => { + // Given + devSessionStatusManager.updateStatus({appEmbedded: false}) + + // When + const renderInstance = render( + , + ) + + await waitForInputsToBeReady() + await sendInputAndWait(renderInstance, 10, 'c') + + // Then + expect(vi.mocked(openURL)).toHaveBeenNthCalledWith(1, 'https://mystore.myshopify.com/admin?dev-console=show') + + renderInstance.unmount() + }) + + test('does not show dev console shortcut when app is embedded', async () => { + // Given + devSessionStatusManager.updateStatus({appEmbedded: true}) + + // When + const renderInstance = render( + , + ) + + await waitForInputsToBeReady() + + // Then + const output = unstyled(renderInstance.lastFrame()!) + expect(output).not.toContain('(c) Open Dev Console') + expect(output).not.toContain('Dev Console URL') + + renderInstance.unmount() + }) + + test('does not show dev console shortcut when app has no extensions', async () => { + // Given + devSessionStatusManager.updateStatus({hasExtensions: false}) + + // When + const renderInstance = render( + , + ) + + await waitForInputsToBeReady() + + // Then + const output = unstyled(renderInstance.lastFrame()!) + expect(output).not.toContain('(c) Open Dev Console') + expect(output).not.toContain('Dev Console URL') + + renderInstance.unmount() + }) + test('quits when q is pressed', async () => { // Given const abortController = new AbortController() @@ -356,7 +434,7 @@ describe('DevSessionUI', () => { await waitForInputsToBeReady() // Initial state - expect(unstyled(renderInstance.lastFrame()!)).not.toContain('preview in your browser') + expect(unstyled(renderInstance.lastFrame()!)).not.toContain('Open app preview') // When status updates devSessionStatusManager.updateStatus({ @@ -365,7 +443,7 @@ describe('DevSessionUI', () => { graphiqlURL: 'https://new-graphiql.shopify.com', }) - await waitForContent(renderInstance, 'Preview in your browser') + await waitForContent(renderInstance, 'Open app preview') // Then expect(unstyled(renderInstance.lastFrame()!)).toContain('Preview URL: https://new-preview-url.shopify.com') diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx index 35f3cf44187..0946bbbca53 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx @@ -7,6 +7,7 @@ import { DevSessionStatusMessageType, } from '../../processes/dev-session/dev-session-status-manager.js' import {MAX_EXTENSION_HANDLE_LENGTH} from '../../../../models/extensions/schemas.js' +import {buildDevConsoleURL} from '../../../../utilities/app/app-url.js' import {OutputProcess} from '@shopify/cli-kit/node/output' import {Alert, ConcurrentOutput, Link, TabularData} from '@shopify/cli-kit/node/ui/components' import {useAbortSignal} from '@shopify/cli-kit/node/ui/hooks' @@ -147,6 +148,16 @@ const DevSessionUI: FunctionComponent = ({ } }, }, + { + key: 'c', + condition: () => Boolean(status.isReady && !status.appEmbedded && status.hasExtensions), + action: async () => { + await metadata.addPublicMetadata(() => ({ + cmd_dev_preview_url_opened: true, + })) + await openURL(buildDevConsoleURL(shopFqdn)) + }, + }, ], content: ( <> @@ -157,14 +168,19 @@ const DevSessionUI: FunctionComponent = ({ )} {canUseShortcuts && ( - {status.graphiqlURL && status.isReady ? ( + {status.isReady ? ( - {figures.pointerSmall} (g) Open GraphiQL (Admin API) in your browser + {figures.pointerSmall} (p) Open app preview ) : null} - {status.isReady ? ( + {status.isReady && !status.appEmbedded && status.hasExtensions ? ( + + {figures.pointerSmall} (c) Open Dev Console for extension previews + + ) : null} + {status.graphiqlURL && status.isReady ? ( - {figures.pointerSmall} (p) Preview in your browser + {figures.pointerSmall} (g) Open GraphiQL (Admin API) ) : null} @@ -181,6 +197,11 @@ const DevSessionUI: FunctionComponent = ({ Preview URL: ) : null} + {status.appEmbedded === false && status.hasExtensions ? ( + + Dev Console URL: + + ) : null} {status.graphiqlURL ? ( GraphiQL URL: diff --git a/packages/app/src/cli/utilities/app/app-url.ts b/packages/app/src/cli/utilities/app/app-url.ts index 79afa5cd860..f201f1be874 100644 --- a/packages/app/src/cli/utilities/app/app-url.ts +++ b/packages/app/src/cli/utilities/app/app-url.ts @@ -13,6 +13,10 @@ export function buildAppURLForAdmin(storeFqdn: string, apiKey: string, adminDoma return `https://${adminDomain}/store/${storeName}/apps/${apiKey}?dev-console=show` } +export function buildDevConsoleURL(storeFqdn: string) { + return `https://${storeFqdn}/admin?dev-console=show` +} + export function buildAppURLForMobile(storeFqdn: string, apiKey: string) { const normalizedFQDN = normalizeStoreFqdn(storeFqdn) const adminUrl = storeAdminUrl(normalizedFQDN) diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 8ccfa480004..89e4cdc7a9c 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -367,6 +367,7 @@ export class AppManagementClient implements DeveloperPlatformClient { organizationId: String(numberFromGid(app.organizationId)), grantedScopes: app.activeRoot.grantedShopifyApprovalScopes, applicationUrl: appHomeModule?.config?.app_url as string | undefined, + embedded: appHomeModule?.config?.embedded as boolean | undefined, flags: [], developerPlatformClient: this, } From a0ce4bffbbde1be0f7dc2100a6d38f53acc2667b Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:19:03 -0400 Subject: [PATCH 2/3] Use nonConfigExtensions for hasExtensions check allExtensions includes config extensions (app_home, webhooks, etc.) that always exist, causing hasExtensions to always be true. Use nonConfigExtensions which only includes user-defined extensions. Co-Authored-By: Claude Opus 4.6 --- .../src/cli/services/dev/processes/dev-session/dev-session.ts | 2 +- .../app/src/cli/services/dev/processes/setup-dev-processes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts index f1747345f52..3c2856e6048 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts @@ -269,7 +269,7 @@ export class DevSession { const hasPreview = event.app.allExtensions.filter((ext) => ext.isPreviewable).length > 0 const useDevConsole = firstPartyDev() && hasPreview const newPreviewURL = useDevConsole ? this.options.appLocalProxyURL : this.options.appPreviewURL - const hasExtensions = event.app.allExtensions.length > 0 + const hasExtensions = event.app.nonConfigExtensions.length > 0 this.statusManager.updateStatus({ previewURL: newPreviewURL, appEmbedded: event.app.configuration.embedded, 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 b7e3390aaa1..0c6ca0b2f6d 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 @@ -126,7 +126,7 @@ export async function setupDevProcesses({ : undefined const appEmbedded = reloadedApp.configuration.embedded - const hasExtensions = reloadedApp.allExtensions.length > 0 + const hasExtensions = reloadedApp.nonConfigExtensions.length > 0 const devSessionStatusManager = new DevSessionStatusManager({ isReady: false, previewURL, From 01d5396b68d2c32856969ea91a93d771440efbb0 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:13:12 -0400 Subject: [PATCH 3/3] Apply suggestions from code review --- .../app/src/cli/services/dev/ui/components/DevSessionUI.tsx | 2 +- packages/app/src/cli/utilities/app/app-url.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx index 0946bbbca53..5dd4144e6a2 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx @@ -150,7 +150,7 @@ const DevSessionUI: FunctionComponent = ({ }, { key: 'c', - condition: () => Boolean(status.isReady && !status.appEmbedded && status.hasExtensions), + condition: () => Boolean(status.isReady && status.appEmbedded === false && status.hasExtensions), action: async () => { await metadata.addPublicMetadata(() => ({ cmd_dev_preview_url_opened: true, diff --git a/packages/app/src/cli/utilities/app/app-url.ts b/packages/app/src/cli/utilities/app/app-url.ts index f201f1be874..53c31c192a9 100644 --- a/packages/app/src/cli/utilities/app/app-url.ts +++ b/packages/app/src/cli/utilities/app/app-url.ts @@ -14,7 +14,8 @@ export function buildAppURLForAdmin(storeFqdn: string, apiKey: string, adminDoma } export function buildDevConsoleURL(storeFqdn: string) { - return `https://${storeFqdn}/admin?dev-console=show` + const normalizedFQDN = normalizeStoreFqdn(storeFqdn) + return `https://${normalizedFQDN}/admin?dev-console=show` } export function buildAppURLForMobile(storeFqdn: string, apiKey: string) {