@@ -50,11 +50,17 @@ export interface DevListenOverrides extends ListenOptions {
5050
5151export interface Listener {
5252 url : string
53+ /** Explicit public URL, or the tunnel or portless URL when there is one. */
54+ publicURL ?: string
55+ /** URL the startup QR code was (or would have been) rendered for. */
56+ qrURL ?: string
5357 address : AddressInfo
5458 server : HttpServer
5559 https : false | ResolvedCertificate
5660 close : ( ) => Promise < void >
5761 getURLs : ( ) => ListenURL [ ]
62+ /** Reprint the URL block, optionally flagging which URL a QR code refers to. */
63+ showURLs : ( options ?: { qr ?: boolean } ) => void
5864}
5965
6066const ANY_HOSTS = new Set ( [ '' , '0.0.0.0' , '::' ] )
@@ -160,47 +166,41 @@ export async function listen(handler: RequestListener, options: ListenOptions =
160166
161167 const publicURL = options . publicURL || tunnel ?. url || portlessShareURL || portlessURL
162168
163- if ( options . showURL !== false ) {
164- const urls = getURLs ( )
165- const qrURL = options . qr === false
166- ? undefined
167- : publicURL
168- || urls . find ( ( { type } ) => type === 'network' ) ?. url
169- || ( options . qr ? url : undefined )
170-
171- if ( qrURL ) {
172- const { renderUnicodeCompact } = await import ( 'uqr' )
173- // eslint-disable-next-line no-console
174- console . log ( `\n${ centerBlock ( renderUnicodeCompact ( qrURL ) ) } \n` )
175- }
169+ const qrURL = options . qr === false
170+ ? undefined
171+ : publicURL
172+ || getURLs ( ) . find ( ( { type } ) => type === 'network' ) ?. url
173+ || ( options . qr ? url : undefined )
176174
175+ function showURLs ( { qr = false } : { qr ?: boolean } = { } ) : void {
176+ const urls = getURLs ( )
177177 const labels = { local : 'Local:' , network : 'Network:' , tunnel : 'Tunnel:' , public : 'Public:' } as const
178178 const labelColors = { local : colors . green , network : colors . magenta , tunnel : colors . cyan , public : colors . magenta } as const
179179 const lines : string [ ] = [ ]
180180 const line = ( color : ( text : string ) => string , label : string , value : string , isQR : boolean ) =>
181181 ` ${ color ( '➜' ) } ${ colors . bold ( color ( label . padEnd ( 10 ) ) ) } ${ value } ${ isQR ? colors . gray ( ' [QR code]' ) : '' } `
182182 for ( const { url : displayURL , type } of urls ) {
183- lines . push ( line ( labelColors [ type ] , labels [ type ] , colors . cyan ( displayURL ) , displayURL === qrURL ) )
183+ lines . push ( line ( labelColors [ type ] , labels [ type ] , colors . cyan ( displayURL ) , qr && displayURL === qrURL ) )
184184 }
185185 if ( ! anyHost && ! tunnel && ! portless . url ) {
186186 lines . push ( line ( colors . magenta , 'Network:' , colors . gray ( `use ${ colors . white ( '--host' ) } to expose` ) , false ) )
187187 }
188188 if ( publicURL && publicURL !== url && ! urls . some ( entry => entry . url === publicURL ) ) {
189- lines . push ( line ( colors . magenta , 'Public:' , colors . cyan ( publicURL ) , publicURL === qrURL ) )
189+ lines . push ( line ( colors . magenta , 'Public:' , colors . cyan ( publicURL ) , qr && publicURL === qrURL ) )
190190 }
191191 // eslint-disable-next-line no-console
192- console . log ( `${ qrURL ? '' : '\n' } ${ lines . join ( '\n' ) } \n` )
192+ console . log ( `${ qr ? '' : '\n' } ${ lines . join ( '\n' ) } \n` )
193193 }
194194
195- if ( options . clipboard ) {
196- try {
197- const { writeText } = await import ( 'tinyclip' )
198- await writeText ( publicURL || url )
199- logger . info ( 'URL copied to clipboard.' )
200- }
201- catch ( error ) {
202- debug ( 'Failed to copy URL to clipboard:' , error )
195+ if ( options . showURL !== false ) {
196+ if ( qrURL ) {
197+ await printQRCode ( qrURL )
203198 }
199+ showURLs ( { qr : ! ! qrURL } )
200+ }
201+
202+ if ( options . clipboard ) {
203+ await copyURL ( publicURL || url )
204204 }
205205
206206 if ( options . open ) {
@@ -209,10 +209,13 @@ export async function listen(handler: RequestListener, options: ListenOptions =
209209
210210 return {
211211 url,
212+ publicURL,
213+ qrURL,
212214 address,
213215 server,
214216 https : certificate ,
215217 getURLs,
218+ showURLs,
216219 close : async ( ) => {
217220 await tunnel ?. close ( )
218221 return new Promise < void > ( ( resolve , reject ) => {
@@ -247,6 +250,24 @@ async function resolvePort(requestedPort: number | undefined, hostname: string,
247250 return port
248251}
249252
253+ export async function printQRCode ( url : string ) : Promise < void > {
254+ const { renderUnicodeCompact } = await import ( 'uqr' )
255+ // eslint-disable-next-line no-console
256+ console . log ( `\n${ centerBlock ( renderUnicodeCompact ( url ) ) } \n` )
257+ }
258+
259+ export async function copyURL ( url : string ) : Promise < void > {
260+ try {
261+ const { writeText } = await import ( 'tinyclip' )
262+ await writeText ( url )
263+ logger . info ( 'URL copied to clipboard.' )
264+ }
265+ catch ( error ) {
266+ debug ( 'Failed to copy URL to clipboard:' , error )
267+ logger . warn ( 'Could not copy the URL to the clipboard.' )
268+ }
269+ }
270+
250271function centerBlock ( block : string ) : string {
251272 const lines = block . split ( '\n' )
252273 const width = Math . max ( ...lines . map ( line => line . length ) )
@@ -303,7 +324,7 @@ export function resolveOpenCommand(
303324 return [ 'xdg-open' , [ url ] ]
304325}
305326
306- function openBrowser ( url : string ) : void {
327+ export function openBrowser ( url : string ) : void {
307328 const resolved = resolveOpenCommand ( url )
308329 if ( ! resolved ) {
309330 return
0 commit comments