Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-hairs-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': minor
---

Render footer links in `app dev` as hyperlinks, if supported by the terminal.
Comment thread
nickwesselman marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import {unstyled} from '@shopify/cli-kit/node/output'
import {openURL} from '@shopify/cli-kit/node/system'
import {Writable} from 'stream'

vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/system', async () => {
const actual: any = await vi.importActual('@shopify/cli-kit/node/system')
return {
...actual,
openURL: vi.fn(),
terminalSupportsHyperlinks: mocks.terminalSupportsHyperlinks,
}
})
vi.mock('@shopify/cli-kit/node/context/local')
vi.mock('@shopify/cli-kit/node/tree-kill')

Expand All @@ -23,6 +30,7 @@ const mocks = vi.hoisted(() => {
useStdin: vi.fn(() => {
return {isRawModeSupported: true}
}),
terminalSupportsHyperlinks: vi.fn(() => false),
}
})

Expand All @@ -48,6 +56,8 @@ const onAbort = vi.fn()

describe('DevSessionUI', () => {
beforeEach(() => {
mocks.terminalSupportsHyperlinks.mockReturnValue(false)
mocks.useStdin.mockReturnValue({isRawModeSupported: true})
devSessionStatusManager = new DevSessionStatusManager()
devSessionStatusManager.reset()
devSessionStatusManager.updateStatus(initialStatus)
Expand Down Expand Up @@ -544,6 +554,62 @@ describe('DevSessionUI', () => {
renderInstance.unmount()
})

test('hides URL list when terminal supports hyperlinks', async () => {
// Given
mocks.terminalSupportsHyperlinks.mockReturnValue(true)

const renderInstance = render(
<DevSessionUI
processes={[]}
abortController={new AbortController()}
devSessionStatusManager={devSessionStatusManager}
shopFqdn="mystore.myshopify.com"
onAbort={onAbort}
/>,
)

await waitForInputsToBeReady()

// Then - shortcuts with label text should be present but URL list should be hidden
const output = unstyled(renderInstance.lastFrame()!)
expect(output).toContain('(p) Open app preview')
expect(output).toContain('(c) Open Dev Console for extension previews')
expect(output).toContain('(g) Open GraphiQL (Admin API)')
expect(output).not.toContain('Preview URL:')
expect(output).not.toContain('Dev Console URL:')
expect(output).not.toContain('GraphiQL URL:')

renderInstance.unmount()
})

test('shows URL list when terminal does not support hyperlinks', async () => {
// Given
mocks.terminalSupportsHyperlinks.mockReturnValue(false)

const renderInstance = render(
<DevSessionUI
processes={[]}
abortController={new AbortController()}
devSessionStatusManager={devSessionStatusManager}
shopFqdn="mystore.myshopify.com"
onAbort={onAbort}
/>,
)

await waitForInputsToBeReady()

// Then - both shortcuts with label text and URL list should be present
const output = unstyled(renderInstance.lastFrame()!)
expect(output).toContain('(p) Open app preview')
expect(output).toContain('(c) Open Dev Console for extension previews')
expect(output).toContain('(g) Open GraphiQL (Admin API)')
expect(output).toContain('Preview URL: https://shopify.com')
expect(output).toContain('Dev Console URL: https://mystore.myshopify.com/admin?dev-console=show')
expect(output).toContain('GraphiQL URL: https://graphiql.shopify.com')

renderInstance.unmount()
})

test('shows non-interactive fallback when raw mode is not supported', async () => {
// Given - mock useStdin to return false for isRawModeSupported
mocks.useStdin.mockReturnValue({isRawModeSupported: false})
Expand All @@ -570,8 +636,5 @@ describe('DevSessionUI', () => {
expect(output).toContain('GraphiQL URL: https://graphiql.shopify.com')

renderInstance.unmount()

// Restore original mock for other tests
mocks.useStdin.mockReturnValue({isRawModeSupported: true})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import React, {FunctionComponent, useEffect, useMemo, useState} from 'react'
import {AbortController, AbortSignal} from '@shopify/cli-kit/node/abort'
import {Box, Text, useInput, useStdin} from '@shopify/cli-kit/node/ink'
import {handleCtrlC} from '@shopify/cli-kit/node/ui'
import {openURL} from '@shopify/cli-kit/node/system'
import {openURL, terminalSupportsHyperlinks} from '@shopify/cli-kit/node/system'
import figures from '@shopify/cli-kit/node/figures'
import {isUnitTest} from '@shopify/cli-kit/node/context/local'
import {treeKill} from '@shopify/cli-kit/node/tree-kill'
Expand Down Expand Up @@ -126,7 +126,7 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
shortcuts: [
{
key: 'p',
condition: () => Boolean(status.previewURL && status.isReady),
condition: () => Boolean(status.isReady && status.previewURL),
action: async () => {
await metadata.addPublicMetadata(() => ({
cmd_dev_preview_url_opened: true,
Expand All @@ -138,7 +138,7 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
},
{
key: 'g',
condition: () => Boolean(status.graphiqlURL && status.isReady),
condition: () => Boolean(status.isReady && status.graphiqlURL),
action: async () => {
await metadata.addPublicMetadata(() => ({
cmd_dev_graphiql_opened: true,
Expand Down Expand Up @@ -168,19 +168,34 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
)}
{canUseShortcuts && (
<Box marginTop={1} flexDirection="column">
{status.isReady ? (
{status.isReady && status.previewURL ? (
<Text>
{figures.pointerSmall} <Text bold>(p)</Text> Open app preview
{figures.pointerSmall} <Text bold>(p)</Text>{' '}
{terminalSupportsHyperlinks() ? (
<Link url={status.previewURL} label="Open app preview" />
) : (
'Open app preview'
)}
</Text>
) : null}
{status.isReady && !status.appEmbedded && status.hasExtensions ? (
<Text>
{figures.pointerSmall} <Text bold>(c)</Text> Open Dev Console for extension previews
{figures.pointerSmall} <Text bold>(c)</Text>{' '}
{terminalSupportsHyperlinks() ? (
<Link url={buildDevConsoleURL(shopFqdn)} label="Open Dev Console for extension previews" />
) : (
'Open Dev Console for extension previews'
)}
</Text>
) : null}
{status.graphiqlURL && status.isReady ? (
{status.isReady && status.graphiqlURL ? (
<Text>
{figures.pointerSmall} <Text bold>(g)</Text> Open GraphiQL (Admin API)
{figures.pointerSmall} <Text bold>(g)</Text>{' '}
{terminalSupportsHyperlinks() ? (
<Link url={status.graphiqlURL} label="Open GraphiQL (Admin API)" />
) : (
'Open GraphiQL (Admin API)'
)}
</Text>
) : null}
</Box>
Expand All @@ -190,7 +205,7 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
<Text>{isShuttingDownMessage}</Text>
) : (
<>
{status.isReady && (
{status.isReady && !(canUseShortcuts && terminalSupportsHyperlinks()) && (
<>
{status.previewURL ? (
<Text>
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/node/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {renderWarning} from './ui.js'
import {platformAndArch} from './os.js'
import {shouldDisplayColors, outputDebug} from './output.js'
import {execa, execaCommand, ExecaChildProcess} from 'execa'
import supportsHyperlinks from 'supports-hyperlinks'
import which from 'which'
import {delimiter} from 'pathe'

Expand Down Expand Up @@ -346,6 +347,15 @@ export async function sleep(seconds: number): Promise<void> {
})
}

/**
* Check if the terminal supports OSC 8 hyperlinks.
*
* @returns True if the terminal supports hyperlinks.
*/
export function terminalSupportsHyperlinks(): boolean {
return supportsHyperlinks.stdout
}

/**
* Check if the standard input and output streams support prompting.
*
Expand Down
Loading