Skip to content

Commit 880c5f8

Browse files
nickwesselmanclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 9dfe97c commit 880c5f8

8 files changed

Lines changed: 134 additions & 10 deletions

File tree

.changeset/blue-taxes-cry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/app': minor
3+
---
4+
5+
Added a separate Dev Console link to the `app dev` output for non-embedded apps

packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface DevSessionStatus {
1717
isReady: boolean
1818
previewURL?: string
1919
graphiqlURL?: string
20+
appEmbedded?: boolean
21+
hasExtensions?: boolean
2022
statusMessage?: {message: string; type: DevSessionStatusMessageType}
2123
}
2224

packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,12 @@ export class DevSession {
265265
const hasPreview = event.app.allExtensions.filter((ext) => ext.isPreviewable).length > 0
266266
const useDevConsole = firstPartyDev() && hasPreview
267267
const newPreviewURL = useDevConsole ? this.options.appLocalProxyURL : this.options.appPreviewURL
268-
this.statusManager.updateStatus({previewURL: newPreviewURL})
268+
const hasExtensions = event.app.allExtensions.length > 0
269+
this.statusManager.updateStatus({
270+
previewURL: newPreviewURL,
271+
appEmbedded: event.app.configuration.embedded,
272+
hasExtensions,
273+
})
269274
}
270275

271276
/**

packages/app/src/cli/services/dev/processes/setup-dev-processes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,15 @@ export async function setupDevProcesses({
125125
? `http://localhost:${graphiqlPort}/graphiql?key=${encodeURIComponent(resolvedGraphiqlKey)}`
126126
: undefined
127127

128-
const devSessionStatusManager = new DevSessionStatusManager({isReady: false, previewURL, graphiqlURL})
128+
const appEmbedded = reloadedApp.configuration.embedded
129+
const hasExtensions = reloadedApp.allExtensions.length > 0
130+
const devSessionStatusManager = new DevSessionStatusManager({
131+
isReady: false,
132+
previewURL,
133+
graphiqlURL,
134+
appEmbedded,
135+
hasExtensions,
136+
})
129137

130138
const processes = [
131139
...(await setupWebProcesses({

packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const initialStatus: DevSessionStatus = {
4040
isReady: true,
4141
previewURL: 'https://shopify.com',
4242
graphiqlURL: 'https://graphiql.shopify.com',
43+
appEmbedded: false,
44+
hasExtensions: true,
4345
}
4446

4547
const onAbort = vi.fn()
@@ -121,10 +123,12 @@ describe('DevSessionUI', () => {
121123
expect(output).toContain('(q) Quit')
122124

123125
// Shortcuts and URLs should be visible
124-
expect(output).toContain('(g) Open GraphiQL')
125-
expect(output).toContain('(p) Preview in your browser')
126+
expect(output).toContain('(g) Open GraphiQL (Admin API)')
127+
expect(output).toContain('(p) Open app preview')
128+
expect(output).toContain('(c) Open Dev Console for extension previews')
126129
expect(output).toContain('Preview URL: https://shopify.com')
127130
expect(output).toContain('GraphiQL URL: https://graphiql.shopify.com')
131+
expect(output).toContain('Dev Console URL: https://mystore.myshopify.com/admin?dev-console=show')
128132

129133
renderInstance.unmount()
130134
})
@@ -171,6 +175,80 @@ describe('DevSessionUI', () => {
171175
renderInstance.unmount()
172176
})
173177

178+
test('opens the dev console URL when c is pressed for non-embedded apps', async () => {
179+
// Given
180+
devSessionStatusManager.updateStatus({appEmbedded: false})
181+
182+
// When
183+
const renderInstance = render(
184+
<DevSessionUI
185+
processes={[]}
186+
abortController={new AbortController()}
187+
devSessionStatusManager={devSessionStatusManager}
188+
shopFqdn="mystore.myshopify.com"
189+
onAbort={onAbort}
190+
/>,
191+
)
192+
193+
await waitForInputsToBeReady()
194+
await sendInputAndWait(renderInstance, 10, 'c')
195+
196+
// Then
197+
expect(vi.mocked(openURL)).toHaveBeenNthCalledWith(1, 'https://mystore.myshopify.com/admin?dev-console=show')
198+
199+
renderInstance.unmount()
200+
})
201+
202+
test('does not show dev console shortcut when app is embedded', async () => {
203+
// Given
204+
devSessionStatusManager.updateStatus({appEmbedded: true})
205+
206+
// When
207+
const renderInstance = render(
208+
<DevSessionUI
209+
processes={[]}
210+
abortController={new AbortController()}
211+
devSessionStatusManager={devSessionStatusManager}
212+
shopFqdn="mystore.myshopify.com"
213+
onAbort={onAbort}
214+
/>,
215+
)
216+
217+
await waitForInputsToBeReady()
218+
219+
// Then
220+
const output = unstyled(renderInstance.lastFrame()!)
221+
expect(output).not.toContain('(c) Open Dev Console')
222+
expect(output).not.toContain('Dev Console URL')
223+
224+
renderInstance.unmount()
225+
})
226+
227+
test('does not show dev console shortcut when app has no extensions', async () => {
228+
// Given
229+
devSessionStatusManager.updateStatus({hasExtensions: false})
230+
231+
// When
232+
const renderInstance = render(
233+
<DevSessionUI
234+
processes={[]}
235+
abortController={new AbortController()}
236+
devSessionStatusManager={devSessionStatusManager}
237+
shopFqdn="mystore.myshopify.com"
238+
onAbort={onAbort}
239+
/>,
240+
)
241+
242+
await waitForInputsToBeReady()
243+
244+
// Then
245+
const output = unstyled(renderInstance.lastFrame()!)
246+
expect(output).not.toContain('(c) Open Dev Console')
247+
expect(output).not.toContain('Dev Console URL')
248+
249+
renderInstance.unmount()
250+
})
251+
174252
test('quits when q is pressed', async () => {
175253
// Given
176254
const abortController = new AbortController()
@@ -356,7 +434,7 @@ describe('DevSessionUI', () => {
356434
await waitForInputsToBeReady()
357435

358436
// Initial state
359-
expect(unstyled(renderInstance.lastFrame()!)).not.toContain('preview in your browser')
437+
expect(unstyled(renderInstance.lastFrame()!)).not.toContain('Open app preview')
360438

361439
// When status updates
362440
devSessionStatusManager.updateStatus({
@@ -365,7 +443,7 @@ describe('DevSessionUI', () => {
365443
graphiqlURL: 'https://new-graphiql.shopify.com',
366444
})
367445

368-
await waitForContent(renderInstance, 'Preview in your browser')
446+
await waitForContent(renderInstance, 'Open app preview')
369447

370448
// Then
371449
expect(unstyled(renderInstance.lastFrame()!)).toContain('Preview URL: https://new-preview-url.shopify.com')

packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DevSessionStatusMessageType,
88
} from '../../processes/dev-session/dev-session-status-manager.js'
99
import {MAX_EXTENSION_HANDLE_LENGTH} from '../../../../models/extensions/schemas.js'
10+
import {buildDevConsoleURL} from '../../../../utilities/app/app-url.js'
1011
import {OutputProcess} from '@shopify/cli-kit/node/output'
1112
import {Alert, ConcurrentOutput, Link, TabularData} from '@shopify/cli-kit/node/ui/components'
1213
import {useAbortSignal} from '@shopify/cli-kit/node/ui/hooks'
@@ -147,6 +148,16 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
147148
}
148149
},
149150
},
151+
{
152+
key: 'c',
153+
condition: () => Boolean(status.isReady && !status.appEmbedded && status.hasExtensions),
154+
action: async () => {
155+
await metadata.addPublicMetadata(() => ({
156+
cmd_dev_preview_url_opened: true,
157+
}))
158+
await openURL(buildDevConsoleURL(shopFqdn))
159+
},
160+
},
150161
],
151162
content: (
152163
<>
@@ -157,14 +168,19 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
157168
)}
158169
{canUseShortcuts && (
159170
<Box marginTop={1} flexDirection="column">
160-
{status.graphiqlURL && status.isReady ? (
171+
{status.isReady ? (
161172
<Text>
162-
{figures.pointerSmall} <Text bold>(g)</Text> Open GraphiQL (Admin API) in your browser
173+
{figures.pointerSmall} <Text bold>(p)</Text> Open app preview
163174
</Text>
164175
) : null}
165-
{status.isReady ? (
176+
{status.isReady && !status.appEmbedded && status.hasExtensions ? (
177+
<Text>
178+
{figures.pointerSmall} <Text bold>(c)</Text> Open Dev Console for extension previews
179+
</Text>
180+
) : null}
181+
{status.graphiqlURL && status.isReady ? (
166182
<Text>
167-
{figures.pointerSmall} <Text bold>(p)</Text> Preview in your browser
183+
{figures.pointerSmall} <Text bold>(g)</Text> Open GraphiQL (Admin API)
168184
</Text>
169185
) : null}
170186
</Box>
@@ -181,6 +197,11 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
181197
Preview URL: <Link url={status.previewURL} />
182198
</Text>
183199
) : null}
200+
{status.appEmbedded === false && status.hasExtensions ? (
201+
<Text>
202+
Dev Console URL: <Link url={buildDevConsoleURL(shopFqdn)} />
203+
</Text>
204+
) : null}
184205
{status.graphiqlURL ? (
185206
<Text>
186207
GraphiQL URL: <Link url={status.graphiqlURL} />

packages/app/src/cli/utilities/app/app-url.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export function buildAppURLForAdmin(storeFqdn: string, apiKey: string, adminDoma
1313
return `https://${adminDomain}/store/${storeName}/apps/${apiKey}?dev-console=show`
1414
}
1515

16+
export function buildDevConsoleURL(storeFqdn: string) {
17+
return `https://${storeFqdn}/admin?dev-console=show`
18+
}
19+
1620
export function buildAppURLForMobile(storeFqdn: string, apiKey: string) {
1721
const normalizedFQDN = normalizeStoreFqdn(storeFqdn)
1822
const adminUrl = storeAdminUrl(normalizedFQDN)

packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ export class AppManagementClient implements DeveloperPlatformClient {
366366
organizationId: String(numberFromGid(app.organizationId)),
367367
grantedScopes: app.activeRoot.grantedShopifyApprovalScopes,
368368
applicationUrl: appHomeModule?.config?.app_url as string | undefined,
369+
embedded: appHomeModule?.config?.embedded as boolean | undefined,
369370
flags: [],
370371
developerPlatformClient: this,
371372
}

0 commit comments

Comments
 (0)