1+ #!/usr/bin/env node
2+
3+ const { default : lighthouse } = require ( 'lighthouse' ) ;
4+ const chromeLauncher = require ( 'chrome-launcher' ) ;
5+ const fs = require ( 'fs' ) . promises ;
6+ const path = require ( 'path' ) ;
7+
8+ // Configuration
9+ const config = {
10+ extends : 'lighthouse:default' ,
11+ settings : {
12+ onlyCategories : [ 'performance' , 'accessibility' , 'best-practices' , 'seo' ] ,
13+ formFactor : 'desktop' ,
14+ throttling : {
15+ rttMs : 40 ,
16+ throughputKbps : 10240 ,
17+ cpuSlowdownMultiplier : 1 ,
18+ requestLatencyMs : 0 ,
19+ downloadThroughputKbps : 0 ,
20+ uploadThroughputKbps : 0
21+ } ,
22+ screenEmulation : {
23+ mobile : false ,
24+ width : 1920 ,
25+ height : 1080 ,
26+ deviceScaleFactor : 1 ,
27+ disabled : false
28+ }
29+ }
30+ } ;
31+
32+ async function runLighthouse ( url ) {
33+ let chrome ;
34+
35+ try {
36+ // Launch Chrome
37+ chrome = await chromeLauncher . launch ( {
38+ chromeFlags : [ '--headless' , '--no-sandbox' , '--disable-dev-shm-usage' ]
39+ } ) ;
40+
41+ const options = {
42+ logLevel : 'info' ,
43+ output : [ 'html' , 'json' ] ,
44+ port : chrome . port
45+ } ;
46+
47+ // Run Lighthouse
48+ const runnerResult = await lighthouse ( url , options , config ) ;
49+
50+ // Create reports directory
51+ const reportsDir = path . join ( __dirname , '..' , 'lighthouse-reports' ) ;
52+ await fs . mkdir ( reportsDir , { recursive : true } ) ;
53+
54+ // Save reports
55+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) ;
56+ const htmlPath = path . join ( reportsDir , `report-${ timestamp } .html` ) ;
57+ const jsonPath = path . join ( reportsDir , `report-${ timestamp } .json` ) ;
58+
59+ await fs . writeFile ( htmlPath , runnerResult . report [ 0 ] ) ;
60+ await fs . writeFile ( jsonPath , runnerResult . report [ 1 ] ) ;
61+
62+ // Parse and display scores
63+ const lhr = runnerResult . lhr ;
64+ console . log ( '\n🚀 Lighthouse Score Report\n' ) ;
65+ console . log ( '=' . repeat ( 50 ) ) ;
66+
67+ const categories = lhr . categories ;
68+ const scores = {
69+ 'Performance' : categories . performance . score * 100 ,
70+ 'Accessibility' : categories . accessibility . score * 100 ,
71+ 'Best Practices' : categories [ 'best-practices' ] . score * 100 ,
72+ 'SEO' : categories . seo . score * 100
73+ } ;
74+
75+ Object . entries ( scores ) . forEach ( ( [ category , score ] ) => {
76+ const emoji = score >= 90 ? '✅' : score >= 50 ? '⚠️' : '❌' ;
77+ const color = score >= 90 ? '\x1b[32m' : score >= 50 ? '\x1b[33m' : '\x1b[31m' ;
78+ console . log ( `${ emoji } ${ category } : ${ color } ${ score . toFixed ( 0 ) } %\x1b[0m` ) ;
79+ } ) ;
80+
81+ console . log ( '=' . repeat ( 50 ) ) ;
82+ console . log ( `\n📊 Full report saved to: ${ htmlPath } ` ) ;
83+ console . log ( `📈 JSON data saved to: ${ jsonPath } \n` ) ;
84+
85+ // Show top opportunities for improvement
86+ if ( lhr . audits ) {
87+ const opportunities = Object . values ( lhr . audits )
88+ . filter ( audit => audit . details ?. type === 'opportunity' && audit . score < 1 )
89+ . sort ( ( a , b ) => ( b . details ?. overallSavingsMs || 0 ) - ( a . details ?. overallSavingsMs || 0 ) )
90+ . slice ( 0 , 5 ) ;
91+
92+ if ( opportunities . length > 0 ) {
93+ console . log ( '\n💡 Top Opportunities for Improvement:\n' ) ;
94+ opportunities . forEach ( ( opp , idx ) => {
95+ const savings = opp . details ?. overallSavingsMs || 0 ;
96+ console . log ( `${ idx + 1 } . ${ opp . title } ` ) ;
97+ if ( savings > 0 ) {
98+ console . log ( ` Potential savings: ${ ( savings / 1000 ) . toFixed ( 2 ) } s` ) ;
99+ }
100+ if ( opp . description ) {
101+ console . log ( ` ${ opp . description . substring ( 0 , 100 ) } ...` ) ;
102+ }
103+ console . log ( ) ;
104+ } ) ;
105+ }
106+ }
107+
108+ // Show failing audits
109+ const failingAudits = Object . values ( lhr . audits )
110+ . filter ( audit => audit . score === 0 && audit . details ?. type !== 'opportunity' )
111+ . slice ( 0 , 5 ) ;
112+
113+ if ( failingAudits . length > 0 ) {
114+ console . log ( '\n⚠️ Issues to Fix:\n' ) ;
115+ failingAudits . forEach ( ( audit , idx ) => {
116+ console . log ( `${ idx + 1 } . ${ audit . title } ` ) ;
117+ if ( audit . description ) {
118+ console . log ( ` ${ audit . description . substring ( 0 , 100 ) } ...` ) ;
119+ }
120+ console . log ( ) ;
121+ } ) ;
122+ }
123+
124+ return scores ;
125+
126+ } catch ( error ) {
127+ console . error ( 'Error running Lighthouse:' , error ) ;
128+ process . exit ( 1 ) ;
129+ } finally {
130+ if ( chrome ) {
131+ await chrome . kill ( ) ;
132+ }
133+ }
134+ }
135+
136+ // Main execution
137+ async function main ( ) {
138+ const url = process . argv [ 2 ] || 'http://localhost:4173/portfolio-v2' ;
139+
140+ console . log ( `\n🔍 Running Lighthouse audit on: ${ url } ` ) ;
141+ console . log ( 'This may take a moment...\n' ) ;
142+
143+ const scores = await runLighthouse ( url ) ;
144+
145+ // Exit with error if any score is below threshold
146+ const threshold = 80 ;
147+ const failedCategories = Object . entries ( scores )
148+ . filter ( ( [ _ , score ] ) => score < threshold ) ;
149+
150+ if ( failedCategories . length > 0 ) {
151+ console . log ( `\n❌ Some categories are below ${ threshold } % threshold` ) ;
152+ process . exit ( 1 ) ;
153+ } else {
154+ console . log ( `\n✅ All categories meet the ${ threshold } % threshold!` ) ;
155+ process . exit ( 0 ) ;
156+ }
157+ }
158+
159+ main ( ) . catch ( console . error ) ;
0 commit comments