@@ -43,7 +43,7 @@ import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/
43
43
import { TunnelClient } from '@shopify/cli-kit/node/plugins/tunnel'
44
44
import { getBackendPort } from '@shopify/cli-kit/node/environment'
45
45
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'
47
47
import { reportAnalyticsEvent } from '@shopify/cli-kit/node/analytics'
48
48
import { OutputProcess , formatPackageManagerCommand , outputDebug } from '@shopify/cli-kit/node/output'
49
49
import { hashString } from '@shopify/cli-kit/node/crypto'
@@ -65,6 +65,19 @@ export interface CustomTunnel {
65
65
}
66
66
67
67
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
+
68
81
export interface DevOptions {
69
82
app : AppLinkedInterface
70
83
remoteApp : OrganizationApp
@@ -84,6 +97,7 @@ export interface DevOptions {
84
97
notify ?: string
85
98
graphiqlPort ?: number
86
99
graphiqlKey ?: string
100
+ portWarnings : PortWarning [ ]
87
101
}
88
102
89
103
export async function dev ( commandOptions : DevOptions ) {
@@ -95,7 +109,7 @@ export async function dev(commandOptions: DevOptions) {
95
109
}
96
110
97
111
async function prepareForDev ( commandOptions : DevOptions ) : Promise < DevConfig > {
98
- const { app, remoteApp, developerPlatformClient, store, specifications} = commandOptions
112
+ const { app, remoteApp, developerPlatformClient, store, specifications, portWarnings } = commandOptions
99
113
100
114
// Be optimistic about tunnel creation and do it as early as possible
101
115
const tunnelPort = await getAvailableTCPPort ( )
@@ -136,23 +150,18 @@ async function prepareForDev(commandOptions: DevOptions): Promise<DevConfig> {
136
150
}
137
151
138
152
const graphiqlPort = commandOptions . graphiqlPort ?? ( await getAvailableTCPPort ( ports . graphiql ) )
139
- const { graphiqlKey } = commandOptions
153
+ const requestedGraphiqlPort = commandOptions . graphiqlPort ?? ports . graphiql
140
154
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' ,
153
160
} )
154
161
}
155
162
163
+ renderPortWarnings ( portWarnings )
164
+
156
165
const { webs, ...network } = await setupNetworkingOptions (
157
166
app . directory ,
158
167
app . webs ,
@@ -189,7 +198,7 @@ async function prepareForDev(commandOptions: DevOptions): Promise<DevConfig> {
189
198
network,
190
199
partnerUrlsUpdated,
191
200
graphiqlPort,
192
- graphiqlKey,
201
+ graphiqlKey : commandOptions . graphiqlKey ,
193
202
}
194
203
}
195
204
@@ -546,10 +555,12 @@ export async function getTunnelMode({
546
555
useLocalhost,
547
556
localhostPort,
548
557
tunnelUrl,
558
+ portWarnings,
549
559
} : {
550
560
tunnelUrl ?: string
551
561
useLocalhost ?: boolean
552
562
localhostPort ?: number
563
+ portWarnings : PortWarning [ ]
553
564
} ) : Promise < TunnelMode > {
554
565
// Developer brought their own tunnel
555
566
if ( tunnelUrl ) {
@@ -573,6 +584,17 @@ export async function getTunnelMode({
573
584
throw new AbortError ( errorMessage , tryMessage )
574
585
}
575
586
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
+
576
598
return {
577
599
mode : 'use-localhost' ,
578
600
port : actualPort ,
@@ -590,26 +612,69 @@ export async function getTunnelMode({
590
612
} ,
591
613
} )
592
614
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
-
609
615
return generateCertificate ( {
610
616
appDirectory,
611
617
onRequiresConfirmation : generateCertificatePrompt ,
612
618
} )
613
619
} ,
614
620
}
615
621
}
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