@@ -50,11 +50,17 @@ export interface DevListenOverrides extends ListenOptions {
5050
5151export interface Listener {
5252url : 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
5357address : AddressInfo
5458server : HttpServer
5559https : false | ResolvedCertificate
5660close : ( ) => Promise < void >
5761getURLs : ( ) => 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
161167const 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 ( )
177177const labels = { local : 'Local:' , network : 'Network:' , tunnel : 'Tunnel:' , public : 'Public:' } as const
178178const labelColors = { local : colors . green , network : colors . magenta , tunnel : colors . cyan , public : colors . magenta } as const
179179const lines : string [ ] = [ ]
180180const 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]' ) : '' } `
182182for ( 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}
185185if ( ! anyHost && ! tunnel && ! portless . url ) {
186186lines . push ( line ( colors . magenta , 'Network:' , colors . gray ( `use ${ colors . white ( '--host' ) } to expose` ) , false ) )
187187}
188188if ( 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
206206if ( options . open ) {
@@ -209,10 +209,13 @@ export async function listen(handler: RequestListener, options: ListenOptions =
209209
210210return {
211211 url,
212+ publicURL,
213+ qrURL,
212214 address,
213215 server,
214216https : certificate ,
215217 getURLs,
218+ showURLs,
216219close : async ( ) => {
217220await tunnel ?. close ( )
218221return new Promise < void > ( ( resolve , reject ) => {
@@ -247,6 +250,24 @@ async function resolvePort(requestedPort: number | undefined, hostname: string,
247250return 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 {
251272const lines = block . split ( '\n' )
252273const width = Math . max ( ...lines . map ( line => line . length ) )
@@ -303,7 +324,7 @@ export function resolveOpenCommand(
303324return [ 'xdg-open' , [ url ] ]
304325}
305326
306- function openBrowser ( url : string ) : void {
327+ export function openBrowser ( url : string ) : void {
307328const resolved = resolveOpenCommand ( url )
308329if ( ! resolved ) {
309330return
0 commit comments