11/**
22 * Main entry point for @vizzly-testing/static-site
33 * Functional orchestration of page discovery and screenshot capture
4+ * Uses a tab pool for efficient browser tab management
45 */
56
6- import {
7- closeBrowser ,
8- closePage ,
9- launchBrowser ,
10- preparePageForScreenshot ,
11- } from './browser.js' ;
12- import { getPageConfig , loadConfig } from './config.js' ;
13- import { discoverPages , generatePageUrl } from './crawler.js' ;
14- import { getBeforeScreenshotHook } from './hooks.js' ;
15- import { captureAndSendScreenshot } from './screenshot.js' ;
7+ import { closeBrowser , launchBrowser } from './browser.js' ;
8+ import { loadConfig } from './config.js' ;
9+ import { discoverPages } from './crawler.js' ;
10+ import { createTabPool } from './pool.js' ;
1611import { startStaticServer , stopStaticServer } from './server.js' ;
17-
18- /**
19- * Process a single page across all configured viewports
20- * @param {Object } page - Page object
21- * @param {Object } browser - Browser instance
22- * @param {string } baseUrl - Base URL for static site (HTTP server)
23- * @param {Object } config - Configuration
24- * @param {Object } context - Plugin context
25- * @returns {Promise<Object> } Result object with success count and errors
26- */
27- async function processPage ( page , browser , baseUrl , config , context ) {
28- let { logger } = context ;
29- let pageConfig = getPageConfig ( config , page ) ;
30- let pageUrl = generatePageUrl ( baseUrl , page ) ;
31- let hook = getBeforeScreenshotHook ( page , config ) ;
32- let errors = [ ] ;
33-
34- // Process each viewport for this page
35- for ( let viewport of pageConfig . viewports ) {
36- let puppeteerPage = null ;
37-
38- try {
39- puppeteerPage = await preparePageForScreenshot (
40- browser ,
41- pageUrl ,
42- viewport ,
43- hook
44- ) ;
45- await captureAndSendScreenshot (
46- puppeteerPage ,
47- page ,
48- viewport ,
49- pageConfig . screenshot
50- ) ;
51-
52- logger . info ( ` ✓ ${ page . path } @${ viewport . name } ` ) ;
53- } catch ( error ) {
54- logger . error ( ` ✗ ${ page . path } @${ viewport . name } : ${ error . message } ` ) ;
55- errors . push ( {
56- page : page . path ,
57- viewport : viewport . name ,
58- error : error . message ,
59- } ) ;
60- } finally {
61- await closePage ( puppeteerPage ) ;
62- }
63- }
64-
65- return { errors } ;
66- }
67-
68- /**
69- * Simple concurrency control - process items with limited parallelism
70- * @param {Array } items - Items to process
71- * @param {Function } fn - Async function to process each item
72- * @param {number } concurrency - Max parallel operations
73- * @returns {Promise<void> }
74- */
75- async function mapWithConcurrency ( items , fn , concurrency ) {
76- let results = [ ] ;
77- let executing = new Set ( ) ;
78-
79- for ( let item of items ) {
80- let promise = fn ( item ) . then ( result => {
81- executing . delete ( promise ) ;
82- return result ;
83- } ) ;
84-
85- results . push ( promise ) ;
86- executing . add ( promise ) ;
87-
88- if ( executing . size >= concurrency ) {
89- await Promise . race ( executing ) ;
90- }
91- }
92-
93- await Promise . all ( results ) ;
94- }
95-
96- /**
97- * Process all pages with concurrency control
98- * @param {Array<Object> } pages - Array of page objects
99- * @param {Object } browser - Browser instance
100- * @param {string } baseUrl - Base URL for static site (HTTP server)
101- * @param {Object } config - Configuration
102- * @param {Object } context - Plugin context
103- * @returns {Promise<Array> } Array of all errors encountered
104- */
105- async function processPages ( pages , browser , baseUrl , config , context ) {
106- let allErrors = [ ] ;
107-
108- await mapWithConcurrency (
109- pages ,
110- async page => {
111- let { errors } = await processPage (
112- page ,
113- browser ,
114- baseUrl ,
115- config ,
116- context
117- ) ;
118- allErrors . push ( ...errors ) ;
119- } ,
120- config . concurrency
121- ) ;
122-
123- return allErrors ;
124- }
12+ import { generateTasks , processAllTasks } from './tasks.js' ;
12513
12614/**
12715 * Check if TDD mode is available
16+ * @param {Function } [debug] - Optional debug logger
12817 * @returns {Promise<boolean> } True if TDD server is running
12918 */
130- async function isTddModeAvailable ( ) {
19+ async function isTddModeAvailable ( debug = ( ) => { } ) {
13120 let { existsSync, readFileSync } = await import ( 'node:fs' ) ;
13221 let { join, parse, dirname } = await import ( 'node:path' ) ;
13322
@@ -136,27 +25,34 @@ async function isTddModeAvailable() {
13625 let currentDir = process . cwd ( ) ;
13726 let root = parse ( currentDir ) . root ;
13827
28+ debug ( `Searching for TDD server from ${ currentDir } ` ) ;
29+
13930 while ( currentDir !== root ) {
14031 let serverJsonPath = join ( currentDir , '.vizzly' , 'server.json' ) ;
14132
14233 if ( existsSync ( serverJsonPath ) ) {
34+ debug ( `Found server.json at ${ serverJsonPath } ` ) ;
14335 try {
14436 let serverInfo = JSON . parse ( readFileSync ( serverJsonPath , 'utf8' ) ) ;
14537 if ( serverInfo . port ) {
38+ debug ( `Pinging TDD server at port ${ serverInfo . port } ` ) ;
14639 // Try to ping the server
14740 let response = await fetch (
14841 `http://localhost:${ serverInfo . port } /health`
14942 ) ;
43+ debug ( `TDD server health check: ${ response . ok ? 'OK' : 'FAILED' } ` ) ;
15044 return response . ok ;
15145 }
152- } catch {
153- // Invalid JSON or server not responding
46+ debug ( 'server.json missing port field' ) ;
47+ } catch ( error ) {
48+ debug ( `Failed to connect to TDD server: ${ error . message } ` ) ;
15449 }
15550 }
15651 currentDir = dirname ( currentDir ) ;
15752 }
158- } catch {
159- // Error checking for TDD mode
53+ debug ( 'No .vizzly/server.json found in parent directories' ) ;
54+ } catch ( error ) {
55+ debug ( `Error checking for TDD mode: ${ error . message } ` ) ;
16056 }
16157
16258 return false ;
@@ -173,6 +69,7 @@ function hasApiToken(config) {
17369
17470/**
17571 * Main run function - orchestrates the entire screenshot capture process
72+ * Uses a tab pool for efficient parallel screenshot capture
17673 * @param {string } buildPath - Path to static site build
17774 * @param {Object } options - CLI options
17875 * @param {Object } context - Plugin context (logger, config, services)
@@ -181,6 +78,7 @@ function hasApiToken(config) {
18178export async function run ( buildPath , options = { } , context = { } ) {
18279 let { logger, config : vizzlyConfig , services } = context ;
18380 let browser = null ;
81+ let pool = null ;
18482 let serverInfo = null ;
18583 let testRunner = null ;
18684 let serverManager = null ;
@@ -196,7 +94,8 @@ export async function run(buildPath, options = {}, context = {}) {
19694 let config = await loadConfig ( buildPath , options , vizzlyConfig ) ;
19795
19896 // Determine mode: TDD or Run
199- let isTdd = await isTddModeAvailable ( ) ;
97+ let debug = logger . debug ?. bind ( logger ) || ( ( ) => { } ) ;
98+ let isTdd = await isTddModeAvailable ( debug ) ;
20099 let hasToken = hasApiToken ( vizzlyConfig ) ;
201100
202101 if ( isTdd ) {
@@ -313,28 +212,27 @@ export async function run(buildPath, options = {}, context = {}) {
313212 return ;
314213 }
315214
316- // Launch browser
215+ // Launch browser and create tab pool
317216 browser = await launchBrowser ( config . browser ) ;
217+ pool = createTabPool ( browser , config . concurrency ) ;
318218
319- // Process all pages
320- let errors = await processPages (
321- pages ,
322- browser ,
323- serverInfo . url ,
324- config ,
325- context
219+ // Generate all tasks upfront (pages × viewports)
220+ let tasks = generateTasks ( pages , serverInfo . url , config ) ;
221+ logger . info (
222+ `📸 Processing ${ tasks . length } screenshots (${ config . concurrency } concurrent tabs)`
326223 ) ;
327224
225+ // Process all tasks through the tab pool
226+ let errors = await processAllTasks ( tasks , pool , config , logger ) ;
227+
328228 // Report summary
329229 if ( errors . length > 0 ) {
330230 logger . warn ( `\n⚠️ ${ errors . length } screenshot(s) failed:` ) ;
331231 errors . forEach ( ( { page, viewport, error } ) => {
332232 logger . error ( ` ${ page } @${ viewport } : ${ error } ` ) ;
333233 } ) ;
334234 } else {
335- logger . info (
336- `\n✅ Captured ${ pages . length * config . viewports . length } screenshots successfully`
337- ) ;
235+ logger . info ( `\n✅ Captured ${ tasks . length } screenshots successfully` ) ;
338236 }
339237
340238 // Finalize build in run mode
@@ -361,7 +259,10 @@ export async function run(buildPath, options = {}, context = {}) {
361259
362260 throw error ;
363261 } finally {
364- // Cleanup
262+ // Cleanup: drain pool first, then close browser
263+ if ( pool ) {
264+ await pool . drain ( ) ;
265+ }
365266 if ( browser ) {
366267 await closeBrowser ( browser ) ;
367268 }
0 commit comments