11#!/usr/bin/env node
2- import React , { useEffect , useState } from "react" ;
2+ import React , { useEffect , useMemo , useRef , useState } from "react" ;
33import { render , Box , Text , useApp } from "ink" ;
4+ import chalk from "chalk" ;
45import {
56 Crawler ,
67 CrawlConfig ,
78 CrawlProgress ,
9+ ScanProgress ,
810 CrawlSummary ,
11+ CrawlStages ,
12+ Progress ,
913} from "./crawler.js" ;
1014
1115import { argsToConfig } from "./config.js" ;
1216
13- interface State {
14- progress: CrawlProgress ;
15- summary ? : CrawlSummary ;
16- startTime: number ;
17- }
18-
1917const config = argsToConfig ( ) ;
2018
2119const Dashboard : React . FC < { cfg : CrawlConfig } > = ( { cfg } ) => {
2220 const { exit } = useApp ( ) ;
23- const [ state , setState ] = useState < State > ( {
24- progress : { phase : "init" } ,
25- startTime : Date . now ( ) ,
21+ const [ phase , setPhase ] = useState < "scanning" | "crawling" | "error" > (
22+ "scanning" ,
23+ ) ;
24+ const [ scanProgress , setScanProgress ] = useState < Partial < ScanProgress >> ( {
25+ queued : 0 ,
26+ finished : 0 ,
27+ url : "" ,
2628 } ) ;
29+ const [ crawlProgress , setCrawlProgress ] = useState < CrawlProgress > ( {
30+ totalPages : 0 ,
31+ finishedPages : 0 ,
32+ url : "" ,
33+ deviceIndex : 0 ,
34+ stageIndex : CrawlStages . Load ,
35+ } ) ;
36+ const [ summary , setSummary ] = useState < CrawlSummary | null > ( null ) ;
37+ const startTimeRef = useRef < number > ( Date . now ( ) ) ;
2738
2839 useEffect ( ( ) => {
2940 const crawler = new Crawler ( cfg ) ;
30- const onProgress = ( p : Partial < CrawlProgress > ) => {
31- setState ( ( s ) => ( { ...s , progress : { ...s . progress , ...p } } ) ) ;
41+ const onProgress = ( p : Progress ) => {
42+ if ( p . phase ) setPhase ( p . phase ) ;
43+ if ( p . message ) {
44+ switch ( p . message . level ) {
45+ case "warning" :
46+ console . log ( chalk . yellow ( p . message . text ) ) ;
47+ break ;
48+ case "error" :
49+ console . log ( chalk . red ( p . message . text ) ) ;
50+ break ;
51+ default : // info
52+ console . log ( p . message . text ) ;
53+ break ;
54+ }
55+ }
56+ if ( p . scanProgress ) {
57+ setScanProgress ( ( s ) => ( { ...s , ...p . scanProgress } ) ) ;
58+ }
59+ if ( p . crawlProgress ) {
60+ setCrawlProgress ( ( s ) => ( { ...s , ...p . crawlProgress } ) ) ;
61+ }
3262 } ;
3363 crawler . on ( "progress" , onProgress ) ;
34- crawler . start ( ) . then ( ( summary ) => {
35- setState ( ( s ) => ( { ...s , summary } ) ) ;
36- setTimeout ( ( ) => {
37- exit ( ) ;
38- process . exit ( 0 ) ;
39- } , 200 ) ; // allow last frame render
40- } ) ;
64+ crawler
65+ . start ( )
66+ . then ( ( s : CrawlSummary ) => {
67+ setSummary ( s ) ;
68+ setTimeout ( ( ) => {
69+ exit ( ) ;
70+ process . exit ( 0 ) ;
71+ } , 200 ) ; // allow last frame render
72+ } )
73+ . catch ( ( e : any ) => {
74+ setPhase ( "error" ) ;
75+ } ) ;
4176 const handleSig = ( ) => {
42- //console.log(isRawModeSupported);
43- //if (isRawModeSupported) setRawMode(false);
44- //spawnSync("stty", ["-a"], { stdio: "inherit" }); // reset terminal after raw mode
45- // the ^[[A on terminal after ctrl-c is caused by node
46- // https://github.com/nodejs/node/issues/41143
4777 crawler . stop ( ) ;
48- //rl.close();
49- //exit();
5078 process . exit ( 0 ) ;
5179 } ;
5280 process . on ( "SIGINT" , handleSig ) ;
@@ -56,11 +84,8 @@ const Dashboard: React.FC<{ cfg: CrawlConfig }> = ({ cfg }) => {
5684 } ) ;
5785 // Extra cleanup on unhandled rejections
5886 process . on ( "unhandledRejection" , ( r ) => {
59- setState ( ( s ) => ( {
60- ...s ,
61- progress : { ...s . progress , phase : "error" , message : String ( r ) } ,
62- } ) ) ;
63- crawler . stop ( ) ;
87+ setPhase ( "error" ) ;
88+ handleSig ( ) ;
6489 } ) ;
6590 return ( ) => {
6691 crawler . stop ( ) ;
@@ -71,58 +96,172 @@ const Dashboard: React.FC<{ cfg: CrawlConfig }> = ({ cfg }) => {
7196 } ;
7297 } , [ cfg , exit ] ) ;
7398
74- const { progress, summary, startTime } = state ;
75- const elapsed = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 1 ) ;
99+ // UI helpers
100+ const stageTexts : Array < NonNullable < string > > = [
101+ "Load" ,
102+ "Cascade" ,
103+ "Cascade Pseudo" ,
104+ ] ;
105+
106+ const devices = useMemo (
107+ ( ) => cfg . deviceWidths . map ( ( w , i ) => ( { index : i , width : w } ) ) ,
108+ [ cfg . deviceWidths ] ,
109+ ) ;
110+ const activeDeviceIndex = crawlProgress . deviceIndex ;
111+ const pagesTotal = crawlProgress . totalPages ;
112+ const finishedPages = crawlProgress . finishedPages ;
113+ const currentStageIndex = crawlProgress . stageIndex ;
114+ const elementsProgressPercent =
115+ ( crawlProgress . totalElements || 0 ) > 0
116+ ? Math . min (
117+ 100 ,
118+ Math . round (
119+ ( ( crawlProgress . processedElements ?? 0 ) /
120+ ( crawlProgress . totalElements || 1 ) ) *
121+ 100 ,
122+ ) ,
123+ )
124+ : undefined ;
125+
126+ const ConfigEntry = ( {
127+ name,
128+ value,
129+ valueColor,
130+ } : {
131+ name : string ;
132+ value: string ;
133+ valueColor ? : string ;
134+ } ) => (
135+ < Text >
136+ { ( name + ":" ) . padEnd ( 16 ) } < Text color = { valueColor } > { value } </ Text >
137+ </ Text >
138+ ) ;
76139
77140 return (
78- < Box
79- flexDirection = "column"
80- padding = { 1 }
81- borderStyle = "round"
82- borderColor = "cyan"
83- >
84- < Text >
85- Crawl < Text color = "green" > { cfg . url } </ Text > { "->" } { " " }
86- < Text color = "yellow" > { cfg . outDir } </ Text > elapsed { elapsed } s
87- </ Text >
88- < Box >
89- < Box flexDirection = "column" width = { 50 } marginRight = { 2 } >
90- < Text >
91- Breakpoints:{ " " }
92- { cfg . breakpoints . length ? cfg . breakpoints . join ( "," ) : "none" }
93- </ Text >
94- { progress . currentUrl && < Text > URL: { progress . currentUrl } </ Text > }
95- { progress . queueSize !== undefined && (
96- < Text > Queue: { progress . queueSize } </ Text >
97- ) }
98- { progress . visitedCount !== undefined && (
99- < Text > Visited: { progress . visitedCount } </ Text >
100- ) }
101- < Text > Phase: { progress . phase } </ Text >
102- { progress . totalElements !== undefined && (
141+ < >
142+ { /* Configs Header */ }
143+ < Box
144+ flexDirection = "column"
145+ marginBottom = { 1 }
146+ padding = { 1 }
147+ paddingTop = { 0 }
148+ borderStyle = "round"
149+ borderColor = "cyan"
150+ >
151+ < Text color = { "cyan" } bold >
152+ Configs:
153+ </ Text >
154+
155+ < ConfigEntry name = "Site URL" value = { cfg . url } valueColor = "green" />
156+ < ConfigEntry name = "Output Dir" value = { cfg . outDir } valueColor = "yellow" />
157+ < ConfigEntry name = "Browser" value = { cfg . browserPath } />
158+ < ConfigEntry
159+ name = "Breakpoints"
160+ value = { cfg . breakpoints . length ? cfg . breakpoints . join ( ", " ) : "none" }
161+ valueColor = "red"
162+ />
163+ < ConfigEntry name = "Device widths" value = { cfg . deviceWidths . join ( ", " ) } />
164+ < ConfigEntry name = "Device Height" value = { String ( cfg . screenHeight ) } />
165+ < ConfigEntry name = "Scale" value = { String ( cfg . deviceScaleFactor ) } />
166+ < ConfigEntry
167+ name = "Delay after nav"
168+ value = { String ( cfg . delayAfterNavigateMs ) + "ms" }
169+ />
170+ < Text >
171+ Other Settings:{ " " }
172+ < Text color = { cfg . recursive ? "" : "gray" } > Recursive</ Text > { " " }
173+ < Text color = { cfg . browserScan ? "" : "gray" } > BrowserScan</ Text > { " " }
174+ < Text color = { cfg . headless ? "" : "gray" } > Headless</ Text >
175+ </ Text >
176+ </ Box >
177+
178+ { /* Progress */ }
179+ { phase === "scanning" && (
180+ < Box flexDirection = "column" marginBottom = { 1 } >
181+ { scanProgress . url && (
103182 < Text >
104- Elements: { progress . processedElements ?? 0 } /
105- { progress . totalElements }
183+ Scanning: < Text color = "magenta" > { scanProgress . url } </ Text >
106184 </ Text >
107185 ) }
108- { progress . resourcesDownloaded !== undefined && (
109- < Text > Resources: { progress . resourcesDownloaded } </ Text >
110- ) }
111- { progress . fontsExtracted !== undefined && (
112- < Text > Fonts: { progress . fontsExtracted } </ Text >
113- ) }
114- { progress . message && < Text color = "gray" > { progress . message } </ Text > }
115- { summary && (
116- < >
117- < Text color = "green" >
118- Done. Pages: { summary . visited . length } Fonts:{ " " }
119- { summary . fontsCssCount }
120- </ Text >
121- </ >
122- ) }
186+ < Text >
187+ Queue: { scanProgress . queued ?? 0 } | Visited:{ " " }
188+ { scanProgress . finished ?? 0 }
189+ </ Text >
123190 </ Box >
124- </ Box >
125- </ Box >
191+ ) }
192+
193+ { phase === "crawling" && (
194+ < >
195+ < Box flexDirection = "column" marginBottom = { 1 } >
196+ < Text >
197+ Page ({ finishedPages + 1 } /{ pagesTotal } ):{ " " }
198+ < Text color = "magenta" > { crawlProgress . url } </ Text >
199+ </ Text >
200+ </ Box >
201+ < Box flexDirection = "column" marginBottom = { 1 } >
202+ < Box rowGap = { 1 } >
203+ { devices . map ( ( d ) => (
204+ < Text
205+ key = { d . index }
206+ backgroundColor = { d . index === activeDeviceIndex ? "cyan" : "" }
207+ color = {
208+ d . index < activeDeviceIndex
209+ ? "green"
210+ : d . index === activeDeviceIndex
211+ ? "black"
212+ : "gray"
213+ }
214+ >
215+ { ` ${ d . width } px ` }
216+ </ Text >
217+ ) ) }
218+ </ Box >
219+ < Box marginBottom = { 1 } >
220+ { stageTexts . map ( ( stgText , i ) => {
221+ const isActive = currentStageIndex === i ;
222+ const color = isActive
223+ ? ""
224+ : ( currentStageIndex ?? - 1 ) > i
225+ ? "green"
226+ : "gray" ;
227+ const decoL = isActive ? "▶" : "" ;
228+ return (
229+ < Box key = { stgText } marginRight = { 2 } >
230+ < Text > { decoL } </ Text >
231+ < Text > </ Text >
232+ < Text color = { color } > { stgText } </ Text >
233+ </ Box >
234+ ) ;
235+ } ) }
236+ </ Box >
237+ { elementsProgressPercent !== undefined &&
238+ ( currentStageIndex === CrawlStages . Cascade ||
239+ currentStageIndex === CrawlStages . CascadePseudo ) && (
240+ < Box paddingLeft = { 2 } >
241+ < Text >
242+ { stageTexts [ currentStageIndex ] } |{ " " }
243+ < Text color = "yellow" >
244+ { elementsProgressPercent } % (
245+ { crawlProgress . processedElements } /
246+ { crawlProgress . totalElements } )
247+ </ Text >
248+ </ Text >
249+ { /*
250+ <ProgressBar value={elementsProgress} />
251+ */ }
252+ </ Box >
253+ ) }
254+ </ Box >
255+ </ >
256+ ) }
257+
258+ { /* Footer / messages */ }
259+ { summary && (
260+ < Text color = "green" >
261+ Done. Pages: { summary . visited . length } Fonts: { summary . fontsCssCount }
262+ </ Text >
263+ ) }
264+ </ >
126265 ) ;
127266} ;
128267
0 commit comments