Skip to content

Commit fb1ba81

Browse files
PR Feedback:
* Fix typo in changelog * If graphiql and localhost port is already in use, we will only show one renderWarning message. We do this by combining 2 messages into 1
1 parent 82261ec commit fb1ba81

File tree

5 files changed

+200
-51
lines changed

5 files changed

+200
-51
lines changed

.changeset/clever-dogs-rush.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'@shopify/app': minor
33
---
44

5-
Improved how port selection when using localhost development
5+
Improved how port selection works when using localhost development
66

77
Added a `--localhost-port` flag. Use this to specify that you want to develop using localhost on a specific port. For example: `shopify app dev --localhost-port=4000`
88

packages/app/src/cli/commands/app/dev.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {appFlags} from '../../flags.js'
2-
import {dev, DevOptions, getTunnelMode} from '../../services/dev.js'
2+
import {dev, DevOptions, getTunnelMode, PortWarning} from '../../services/dev.js'
33
import {showApiKeyDeprecationWarning} from '../../prompts/deprecation-warnings.js'
44
import {checkFolderIsValidApp} from '../../models/app/loader.js'
55
import AppCommand, {AppCommandOutput} from '../../utilities/app-command.js'
@@ -141,10 +141,15 @@ If you're using the Ruby app template, then you need to complete the following s
141141
await showApiKeyDeprecationWarning()
142142
}
143143

144+
// getTunnelMode can populate this array. This array is then passed to the dev function.
145+
// This allows the dev function to group port warnings into a single renderWarning
146+
const portWarnings: PortWarning[] = []
147+
144148
const tunnelMode = await getTunnelMode({
145149
useLocalhost: flags['use-localhost'],
146150
tunnelUrl: flags['tunnel-url'],
147151
localhostPort: flags['localhost-port'],
152+
portWarnings,
148153
})
149154

150155
await addPublicMetadata(() => {
@@ -184,6 +189,7 @@ If you're using the Ruby app template, then you need to complete the following s
184189
graphiqlPort: flags['graphiql-port'],
185190
graphiqlKey: flags['graphiql-key'],
186191
tunnel: tunnelMode,
192+
portWarnings,
187193
}
188194

189195
await dev(devOptions)

packages/app/src/cli/services/dev.test.ts

+90-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
developerPreviewController,
55
getTunnelMode,
66
NoTunnel,
7+
PortWarning,
8+
renderPortWarnings,
79
warnIfScopesDifferBeforeDev,
810
} from './dev.js'
911
import {fetchAppPreviewMode} from './dev/fetch.js'
@@ -110,11 +112,75 @@ describe('warnIfScopesDifferBeforeDev', () => {
110112
})
111113
})
112114

115+
describe('renderPortWarnings()', () => {
116+
test('does not call renderWarning when no warnings', () => {
117+
// Given
118+
const mockOutput = mockAndCaptureOutput()
119+
const portWarnings: PortWarning[] = []
120+
121+
// When
122+
mockOutput.clear()
123+
renderPortWarnings(portWarnings)
124+
125+
// Then
126+
expect(mockOutput.warn()).toBe('')
127+
})
128+
129+
test('calls renderWarning when there is a warning', () => {
130+
// Given
131+
const mockOutput = mockAndCaptureOutput()
132+
const portWarnings: PortWarning[] = [
133+
{
134+
type: 'localhost',
135+
flag: '--localhost-port',
136+
requestedPort: 1234,
137+
},
138+
]
139+
140+
// When
141+
mockOutput.clear()
142+
renderPortWarnings(portWarnings)
143+
144+
// Then
145+
expect(mockOutput.warn()).toContain('A random port will be used for localhost because 1234 is not available.')
146+
expect(mockOutput.warn()).toContain('If you want to use a specific port, you can choose a different one by')
147+
expect(mockOutput.warn()).toContain('setting the `--localhost-port` flag.')
148+
})
149+
150+
test('Combines warnings when there are multiple', () => {
151+
// Given
152+
const mockOutput = mockAndCaptureOutput()
153+
const portWarnings: PortWarning[] = [
154+
{
155+
type: 'localhost',
156+
flag: '--localhost-port',
157+
requestedPort: 1234,
158+
},
159+
{
160+
type: 'GraphiQL',
161+
flag: '--graphiql-port',
162+
requestedPort: 5678,
163+
},
164+
]
165+
166+
// When
167+
mockOutput.clear()
168+
renderPortWarnings(portWarnings)
169+
170+
// Then
171+
expect(mockOutput.warn()).toContain('Random ports will be used for localhost and GraphiQL because the requested')
172+
expect(mockOutput.warn()).toContain('ports are not available')
173+
expect(mockOutput.warn()).toContain('If you want to use specific ports, you can choose different ports using')
174+
expect(mockOutput.warn()).toContain('the `--localhost-port` and `--graphiql-port` flags.')
175+
})
176+
})
177+
113178
describe('getTunnelMode() if tunnelUrl is defined', () => {
114179
const defaultOptions = {
115180
useLocalhost: false,
116181
localhostPort: undefined,
117182
tunnelUrl: undefined,
183+
portWarnings: [],
118184
}
119185

120186
test('returns AutoTunnel', async () => {
@@ -135,6 +201,7 @@ describe('getTunnelMode() if useLocalhost is false and tunnelUrl is a string', (
135201
useLocalhost: false,
136202
localhostPort: undefined,
137203
tunnelUrl: 'https://my-tunnel-url.com',
204+
portWarnings: [],
138205
}
139206

140207
test('returns CustomTunnel', async () => {
@@ -168,6 +235,7 @@ describe('getTunnelMode() if useLocalhost is true', () => {
168235
useLocalhost: true,
169236
localhostPort: undefined,
170237
tunnelUrl: undefined,
238+
portWarnings: [],
171239
}
172240

173241
test('returns localhostPort when passed', async () => {
@@ -220,6 +288,28 @@ describe('getTunnelMode() if useLocalhost is true', () => {
220288
})
221289
})
222290

291+
test('Warns if ports.localhost is not available', async () => {
292+
// Given
293+
const availablePort = ports.localhost + 1
294+
vi.mocked(checkPortAvailability).mockResolvedValue(false)
295+
vi.mocked(getAvailableTCPPort).mockResolvedValue(ports.localhost + 1)
296+
const portWarnings: PortWarning[] = []
297+
298+
// When
299+
const result = (await getTunnelMode({...defaultOptions, portWarnings})) as NoTunnel
300+
await result.provideCertificate('app-directory')
301+
302+
// Then
303+
expect(result.port).toBe(availablePort)
304+
expect(portWarnings).toStrictEqual([
305+
{
306+
type: 'localhost',
307+
flag: '--localhost-port',
308+
requestedPort: ports.localhost,
309+
},
310+
])
311+
})
312+
223313
describe('provideCertificate()', () => {
224314
test('Calls renderInfo', async () => {
225315
// Given
@@ -235,23 +325,6 @@ describe('getTunnelMode() if useLocalhost is true', () => {
235325
expect(mockOutput.info()).toContain('Localhost-based development is in developer preview')
236326
})
237327

238-
test('Renders warning if ports.localhost is not available', async () => {
239-
// Given
240-
const availablePort = ports.localhost + 1
241-
vi.mocked(checkPortAvailability).mockResolvedValue(false)
242-
vi.mocked(getAvailableTCPPort).mockResolvedValue(ports.localhost + 1)
243-
244-
// When
245-
const mockOutput = mockAndCaptureOutput()
246-
mockOutput.clear()
247-
const result = (await getTunnelMode(defaultOptions)) as NoTunnel
248-
await result.provideCertificate('app-directory')
249-
250-
// Then
251-
expect(result.port).toBe(availablePort)
252-
expect(mockOutput.warn()).toContain('A random port will be used for localhost')
253-
})
254-
255328
test('Calls generateCertificate and returns its value', async () => {
256329
// Given
257330
vi.mocked(checkPortAvailability).mockResolvedValue(true)

packages/app/src/cli/services/dev.ts

+97-32
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/
4343
import {TunnelClient} from '@shopify/cli-kit/node/plugins/tunnel'
4444
import {getBackendPort} from '@shopify/cli-kit/node/environment'
4545
import {basename} from '@shopify/cli-kit/node/path'
46-
import {renderWarning, renderInfo} from '@shopify/cli-kit/node/ui'
46+
import {renderWarning, renderInfo, Token} from '@shopify/cli-kit/node/ui'
4747
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
4848
import {OutputProcess, formatPackageManagerCommand, outputDebug} from '@shopify/cli-kit/node/output'
4949
import {hashString} from '@shopify/cli-kit/node/crypto'
@@ -65,6 +65,19 @@ export interface CustomTunnel {
6565
}
6666

6767
type TunnelMode = NoTunnel | AutoTunnel | CustomTunnel
68+
export type PortWarning = (
69+
| {
70+
type: 'GraphiQL'
71+
flag: '--graphiql-port'
72+
}
73+
| {
74+
type: 'localhost'
75+
flag: '--localhost-port'
76+
}
77+
) & {
78+
requestedPort: number
79+
}
80+
6881
export interface DevOptions {
6982
app: AppLinkedInterface
7083
remoteApp: OrganizationApp
@@ -84,6 +97,7 @@ export interface DevOptions {
8497
notify?: string
8598
graphiqlPort?: number
8699
graphiqlKey?: string
100+
portWarnings: PortWarning[]
87101
}
88102

89103
export async function dev(commandOptions: DevOptions) {
@@ -95,7 +109,7 @@ export async function dev(commandOptions: DevOptions) {
95109
}
96110

97111
async function prepareForDev(commandOptions: DevOptions): Promise<DevConfig> {
98-
const {app, remoteApp, developerPlatformClient, store, specifications} = commandOptions
112+
const {app, remoteApp, developerPlatformClient, store, specifications, portWarnings} = commandOptions
99113

100114
// Be optimistic about tunnel creation and do it as early as possible
101115
const tunnelPort = await getAvailableTCPPort()
@@ -136,23 +150,18 @@ async function prepareForDev(commandOptions: DevOptions): Promise<DevConfig> {
136150
}
137151

138152
const graphiqlPort = commandOptions.graphiqlPort ?? (await getAvailableTCPPort(ports.graphiql))
139-
const {graphiqlKey} = commandOptions
153+
const requestedGraphiqlPort = commandOptions.graphiqlPort ?? ports.graphiql
140154

141-
if (graphiqlPort !== (commandOptions.graphiqlPort ?? ports.graphiql)) {
142-
renderWarning({
143-
headline: [
144-
'A random port will be used for GraphiQL because',
145-
{command: `${ports.graphiql}`},
146-
'is not available.',
147-
],
148-
body: [
149-
'If you want to keep your session in GraphiQL, you can choose a different one by setting the',
150-
{command: '--graphiql-port'},
151-
'flag.',
152-
],
155+
if (graphiqlPort !== requestedGraphiqlPort) {
156+
portWarnings.push({
157+
type: 'GraphiQL',
158+
requestedPort: requestedGraphiqlPort,
159+
flag: '--graphiql-port',
153160
})
154161
}
155162

163+
renderPortWarnings(portWarnings)
164+
156165
const {webs, ...network} = await setupNetworkingOptions(
157166
app.directory,
158167
app.webs,
@@ -189,7 +198,7 @@ async function prepareForDev(commandOptions: DevOptions): Promise<DevConfig> {
189198
network,
190199
partnerUrlsUpdated,
191200
graphiqlPort,
192-
graphiqlKey,
201+
graphiqlKey: commandOptions.graphiqlKey,
193202
}
194203
}
195204

@@ -546,10 +555,12 @@ export async function getTunnelMode({
546555
useLocalhost,
547556
localhostPort,
548557
tunnelUrl,
558+
portWarnings,
549559
}: {
550560
tunnelUrl?: string
551561
useLocalhost?: boolean
552562
localhostPort?: number
563+
portWarnings: PortWarning[]
553564
}): Promise<TunnelMode> {
554565
// Developer brought their own tunnel
555566
if (tunnelUrl) {
@@ -573,6 +584,17 @@ export async function getTunnelMode({
573584
throw new AbortError(errorMessage, tryMessage)
574585
}
575586

587+
// The user didn't specify a port. The default isn't available. Add to warnings array
588+
// This will be rendered using renderWarning later when dev() is called
589+
// This allows us to consolidate all port warnings into one renderWarning message
590+
if (requestedPort !== actualPort) {
591+
portWarnings.push({
592+
type: 'localhost',
593+
requestedPort,
594+
flag: '--localhost-port',
595+
})
596+
}
597+
576598
return {
577599
mode: 'use-localhost',
578600
port: actualPort,
@@ -590,26 +612,69 @@ export async function getTunnelMode({
590612
},
591613
})
592614

593-
// The user didn't specify a port. The default isn't available. Warn
594-
if (requestedPort !== actualPort) {
595-
renderWarning({
596-
headline: [
597-
'A random port will be used for localhost because',
598-
{command: `${requestedPort}`},
599-
'is not available.',
600-
],
601-
body: [
602-
'If you want to use a specific port, choose a different one or free up the one you requested. Then re-run the command with the',
603-
{command: '--localhost-port PORT'},
604-
'flag.',
605-
],
606-
})
607-
}
608-
609615
return generateCertificate({
610616
appDirectory,
611617
onRequiresConfirmation: generateCertificatePrompt,
612618
})
613619
},
614620
}
615621
}
622+
623+
export function renderPortWarnings(portWarnings: PortWarning[] = []) {
624+
if (portWarnings.length === 0 || !portWarnings[0]) return
625+
626+
if (portWarnings.length === 1) {
627+
const warning = portWarnings[0]
628+
629+
renderWarning({
630+
headline: [`A random port will be used for ${warning.type} because ${warning?.requestedPort} is not available.`],
631+
body: [
632+
`If you want to use a specific port, you can choose a different one by setting the `,
633+
{command: warning?.flag},
634+
` flag.`,
635+
],
636+
})
637+
return
638+
}
639+
640+
const formattedWarningTypes = asHumanFriendlyTokenList(portWarnings.map((warning) => warning.type)).join(' ')
641+
const formattedFlags = asHumanFriendlyTokenList(portWarnings.map((warning) => ({command: warning.flag})))
642+
643+
renderWarning({
644+
headline: [`Random ports will be used for ${formattedWarningTypes} because the requested ports are not available.`],
645+
body: [`If you want to use specific ports, you can choose different ports using the`, ...formattedFlags, `flags.`],
646+
})
647+
}
648+
649+
/**
650+
* Converts an array of Tokens into a human friendly list
651+
*
652+
* Returns a new array that contains the items separated by commas,
653+
* except for the last item, which is seperated by "and".
654+
* This is useful for creating human-friendly sentences.
655+
*
656+
* @example
657+
* ```ts
658+
* const items = ['apple', 'banana', 'cherry'];
659+
* const result = asHumanFriendlyList(items)
660+
*
661+
* //['apple', ',', 'banana', ',', 'and', 'cherry']
662+
* console.log(result);
663+
* ```
664+
*/
665+
666+
function asHumanFriendlyTokenList(items: Token[]): Token[] {
667+
if (items.length < 2) {
668+
return items
669+
}
670+
671+
return items.reduce<Token[]>((acc, item, index) => {
672+
if (index === items.length - 1) {
673+
acc.push('and')
674+
} else if (index !== 0) {
675+
acc.push(', ')
676+
}
677+
acc.push(item)
678+
return acc
679+
}, [])
680+
}

0 commit comments

Comments
 (0)