11#!/usr/bin/env node
22
33import * as fs from 'fs' ;
4+ import * as os from 'os' ;
5+ import * as kleur from 'kleur/colors' ;
6+ import logUpdate from 'log-update' ;
47import * as path from 'path' ;
58import sade from 'sade' ;
69import { scan } from './scan-task' ;
@@ -16,6 +19,11 @@ It may in the future make sense to use a bundler to combine all the dist/ files
1619// eslint-disable-next-line @cloudfour/typescript-eslint/no-var-requires
1720const { version } = require ( '../package.json' ) ;
1821
22+ const symbols = {
23+ error : kleur . red ( '✖' ) ,
24+ success : kleur . green ( '✔' ) ,
25+ } ;
26+
1927sade ( 'lighthouse-parade <url> [dataDirectory]' , true )
2028 . version ( version )
2129 . describe (
@@ -30,6 +38,11 @@ sade('lighthouse-parade <url> [dataDirectory]', true)
3038 '--crawler-user-agent' ,
3139 'Pass a user agent string to be used by the crawler (not by Lighthouse)'
3240 )
41+ . option (
42+ '--lighthouse-concurrency' ,
43+ 'Control the maximum number of ligthhouse reports to run concurrently' ,
44+ os . cpus ( ) . length - 1
45+ )
3346 . action (
3447 (
3548 url ,
@@ -54,14 +67,96 @@ sade('lighthouse-parade <url> [dataDirectory]', true)
5467 throw new Error ( '--crawler-user-agent flag must be a string' ) ;
5568 }
5669
57- const scanner = scan ( url , { ignoreRobotsTxt, dataDirectory } ) ;
70+ const lighthouseConcurrency = opts [ 'lighthouse-concurrency' ] ;
71+
72+ const scanner = scan ( url , {
73+ ignoreRobotsTxt,
74+ dataDirectory,
75+ lighthouseConcurrency,
76+ } ) ;
77+
78+ const enum State {
79+ Pending,
80+ ReportInProgress,
81+ ReportComplete,
82+ }
83+ const urlStates = new Map <
84+ string ,
85+ { state : State ; error ? : Error | string }
86+ > ( ) ;
87+
88+ const frames = [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ] ;
89+ let i = 0 ;
90+
91+ const printLine = ( url : string , state : State , error ? : Error | string ) => {
92+ const frame = kleur . blue ( frames [ i ] ) ;
93+ const statusIcon = error
94+ ? symbols . error
95+ : state === State . Pending
96+ ? ' '
97+ : state === State . ReportInProgress
98+ ? frame
99+ : symbols . success ;
100+ let output = `${ statusIcon } ${ url } ` ;
101+ if ( error ) {
102+ output += `\n ${ kleur . gray ( error . toString ( ) ) } ` ;
103+ }
104+
105+ return output ;
106+ } ;
107+
108+ const render = ( ) => {
109+ const pendingUrls : string [ ] = [ ] ;
110+ const currentUrls : string [ ] = [ ] ;
111+ urlStates . forEach ( ( { state, error } , url ) => {
112+ if ( state === State . ReportComplete ) return ;
113+ const line = `${ printLine ( url , state , error ) } \n` ;
114+ if ( state === State . Pending ) pendingUrls . push ( line ) ;
115+ else currentUrls . push ( line ) ;
116+ } ) ;
117+ const numPendingToDisplay = Math . min (
118+ Math . max ( process . stdout . rows - currentUrls . length - 3 , 1 ) ,
119+ pendingUrls . length
120+ ) ;
121+ const numHiddenUrls =
122+ numPendingToDisplay === pendingUrls . length
123+ ? ''
124+ : kleur . dim (
125+ `\n...And ${
126+ pendingUrls . length - numPendingToDisplay
127+ } more pending`
128+ ) ;
129+ logUpdate (
130+ currentUrls . join ( '' ) +
131+ pendingUrls . slice ( 0 , numPendingToDisplay ) . join ( '' ) +
132+ numHiddenUrls
133+ ) ;
134+ } ;
135+
136+ const intervalId = setInterval ( ( ) => {
137+ i = ( i + 1 ) % frames . length ;
138+ render ( ) ;
139+ } , 80 ) ;
140+
141+ /**
142+ * Allows you to run a console.log that will output _above_ the persistent logUpdate log
143+ * Pass a callback where you run your console.log or console.error
144+ */
145+ const printAboveLogUpdate = ( cb : ( ) = > void ) => {
146+ logUpdate . clear ( ) ;
147+ cb ( ) ;
148+ render ( ) ;
149+ } ;
150+
151+ const log = ( ...messages : string [ ] ) =>
152+ printAboveLogUpdate ( ( ) => console . log ( ...messages ) ) ;
58153
59154 const urlsFile = path . join ( dataDirectory , 'urls.csv' ) ;
60155 fs . writeFileSync ( urlsFile , 'URL,content_type,bytes,response\n' ) ;
61156 const urlsStream = fs . createWriteStream ( urlsFile , { flags : 'a' } ) ;
62157
63158 scanner . on ( 'urlFound' , ( url , contentType , bytes , statusCode ) => {
64- console . log ( 'Crawled %s [%s] (%d bytes)' , url , contentType , bytes ) ;
159+ urlStates . set ( url , { state : State . Pending } ) ;
65160 const csvLine = [
66161 JSON . stringify ( url ) ,
67162 contentType ,
@@ -70,15 +165,28 @@ sade('lighthouse-parade <url> [dataDirectory]', true)
70165 ] . join ( ',' ) ;
71166 urlsStream . write ( `${ csvLine } \n` ) ;
72167 } ) ;
168+ scanner . on ( 'reportBegin' , ( url ) => {
169+ urlStates . set ( url , { state : State . ReportInProgress } ) ;
170+ } ) ;
171+ scanner . on ( 'reportFail' , ( url , error ) => {
172+ urlStates . set ( url , { state : State . ReportComplete , error } ) ;
173+ log ( printLine ( url , State . ReportComplete , error ) ) ;
174+ } ) ;
73175 scanner . on ( 'reportComplete' , ( url , reportData ) => {
74- console . log ( 'Report is done for' , url ) ;
176+ urlStates . set ( url , { state : State . ReportComplete } ) ;
177+ log ( printLine ( url , State . ReportComplete ) ) ;
75178 const reportFileName = makeFileNameFromUrl ( url , 'csv' ) ;
76179
77180 fs . writeFileSync ( path . join ( reportsDirPath , reportFileName ) , reportData ) ;
78181 } ) ;
79182
80183 scanner . on ( 'info' , ( message ) => {
81- console . log ( message ) ;
184+ log ( message ) ;
185+ } ) ;
186+
187+ scanner . promise . then ( ( ) => {
188+ clearInterval ( intervalId ) ;
189+ console . log ( 'DONE!' ) ;
82190 } ) ;
83191 }
84192 )
0 commit comments