11import * as os from 'os' ;
22import * as uniqueFilename from 'unique-filename' ;
33import * as puppeteer from 'puppeteer' ;
4+ import * as chokidar from 'chokidar' ;
5+ import * as path from 'path' ;
6+ import * as fs from 'fs' ;
47import { Logger } from '../logger' ;
58import { RenderingConfig } from '../config' ;
69
@@ -23,10 +26,26 @@ export interface RenderOptions {
2326 headers ?: HTTPHeaders ;
2427}
2528
29+ export interface RenderCSVOptions {
30+ url : string ;
31+ filePath : string ;
32+ timeout : string | number ;
33+ renderKey : string ;
34+ domain : string ;
35+ timezone ?: string ;
36+ encoding ?: string ;
37+ headers ?: HTTPHeaders ;
38+ }
39+
2640export interface RenderResponse {
2741 filePath : string ;
2842}
2943
44+ export interface RenderCSVResponse {
45+ filePath : string ;
46+ fileName ?: string ;
47+ }
48+
3049export class Browser {
3150 constructor ( protected config : RenderingConfig , protected log : Logger ) {
3251 this . log . debug ( 'Browser initialized' , 'config' , this . config ) ;
@@ -48,15 +67,31 @@ export class Browser {
4867
4968 async start ( ) : Promise < void > { }
5069
51- validateOptions ( options : RenderOptions ) {
70+ validateRenderOptions ( options : RenderOptions | RenderCSVOptions ) {
5271 if ( options . url . startsWith ( `socket://` ) ) {
5372 // Puppeteer doesn't support socket:// URLs
5473 throw new Error ( `Image rendering in socket mode is not supported` ) ;
5574 }
5675
76+ options . headers = options . headers || { } ;
77+ const headers = { } ;
78+
79+ if ( options . headers [ 'Accept-Language' ] ) {
80+ headers [ 'Accept-Language' ] = options . headers [ 'Accept-Language' ] ;
81+ } else if ( this . config . acceptLanguage ) {
82+ headers [ 'Accept-Language' ] = this . config . acceptLanguage ;
83+ }
84+
85+ options . headers = headers ;
86+
87+ options . timeout = parseInt ( options . timeout as string , 10 ) || 30 ;
88+ }
89+
90+ validateImageOptions ( options : RenderOptions ) {
91+ this . validateRenderOptions ( options ) ;
92+
5793 options . width = parseInt ( options . width as string , 10 ) || this . config . width ;
5894 options . height = parseInt ( options . height as string , 10 ) || this . config . height ;
59- options . timeout = parseInt ( options . timeout as string , 10 ) || 30 ;
6095
6196 if ( options . width < 10 ) {
6297 options . width = this . config . width ;
@@ -79,17 +114,6 @@ export class Browser {
79114 if ( options . deviceScaleFactor > this . config . maxDeviceScaleFactor ) {
80115 options . deviceScaleFactor = this . config . deviceScaleFactor ;
81116 }
82-
83- options . headers = options . headers || { } ;
84- const headers = { } ;
85-
86- if ( options . headers [ 'Accept-Language' ] ) {
87- headers [ 'Accept-Language' ] = options . headers [ 'Accept-Language' ] ;
88- } else if ( this . config . acceptLanguage ) {
89- headers [ 'Accept-Language' ] = this . config . acceptLanguage ;
90- }
91-
92- options . headers = headers ;
93117 }
94118
95119 getLauncherOptions ( options ) {
@@ -111,12 +135,28 @@ export class Browser {
111135 return launcherOptions ;
112136 }
113137
138+ async preparePage ( page : any , options : any ) {
139+ if ( this . config . verboseLogging ) {
140+ this . log . debug ( 'Setting cookie for page' , 'renderKey' , options . renderKey , 'domain' , options . domain ) ;
141+ }
142+ await page . setCookie ( {
143+ name : 'renderKey' ,
144+ value : options . renderKey ,
145+ domain : options . domain ,
146+ } ) ;
147+
148+ if ( options . headers && Object . keys ( options . headers ) . length > 0 ) {
149+ this . log . debug ( `Setting extra HTTP headers for page` , 'headers' , options . headers ) ;
150+ await page . setExtraHTTPHeaders ( options . headers ) ;
151+ }
152+ }
153+
114154 async render ( options : RenderOptions ) : Promise < RenderResponse > {
115155 let browser ;
116156 let page : any ;
117157
118158 try {
119- this . validateOptions ( options ) ;
159+ this . validateImageOptions ( options ) ;
120160 const launcherOptions = this . getLauncherOptions ( options ) ;
121161 browser = await puppeteer . launch ( launcherOptions ) ;
122162 page = await browser . newPage ( ) ;
@@ -152,19 +192,7 @@ export class Browser {
152192 deviceScaleFactor : options . deviceScaleFactor ,
153193 } ) ;
154194
155- if ( this . config . verboseLogging ) {
156- this . log . debug ( 'Setting cookie for page' , 'renderKey' , options . renderKey , 'domain' , options . domain ) ;
157- }
158- await page . setCookie ( {
159- name : 'renderKey' ,
160- value : options . renderKey ,
161- domain : options . domain ,
162- } ) ;
163-
164- if ( options . headers && Object . keys ( options . headers ) . length > 0 ) {
165- this . log . debug ( `Setting extra HTTP headers for page` , 'headers' , options . headers ) ;
166- await page . setExtraHTTPHeaders ( options . headers ) ;
167- }
195+ await this . preparePage ( page , options ) ;
168196
169197 if ( this . config . verboseLogging ) {
170198 this . log . debug ( 'Moving mouse on page' , 'x' , options . width , 'y' , options . height ) ;
@@ -202,6 +230,78 @@ export class Browser {
202230 return { filePath : options . filePath } ;
203231 }
204232
233+ async renderCSV ( options : RenderCSVOptions ) : Promise < RenderCSVResponse > {
234+ let browser ;
235+ let page : any ;
236+
237+ try {
238+ this . validateRenderOptions ( options ) ;
239+ const launcherOptions = this . getLauncherOptions ( options ) ;
240+ browser = await puppeteer . launch ( launcherOptions ) ;
241+ page = await browser . newPage ( ) ;
242+ this . addPageListeners ( page ) ;
243+
244+ return await this . exportCSV ( page , options ) ;
245+ } finally {
246+ if ( page ) {
247+ this . removePageListeners ( page ) ;
248+ await page . close ( ) ;
249+ }
250+ if ( browser ) {
251+ await browser . close ( ) ;
252+ }
253+ }
254+ }
255+
256+ async exportCSV ( page : any , options : any ) : Promise < RenderCSVResponse > {
257+ await this . preparePage ( page , options ) ;
258+
259+ const downloadPath = uniqueFilename ( os . tmpdir ( ) ) ;
260+ fs . mkdirSync ( downloadPath ) ;
261+ const watcher = chokidar . watch ( downloadPath ) ;
262+ let downloadFilePath = '' ;
263+ watcher . on ( 'add' , file => {
264+ if ( ! file . endsWith ( '.crdownload' ) ) {
265+ downloadFilePath = file ;
266+ }
267+ } ) ;
268+
269+ await page . _client . send ( 'Page.setDownloadBehavior' , { behavior : 'allow' , downloadPath : downloadPath } ) ;
270+
271+ if ( this . config . verboseLogging ) {
272+ this . log . debug ( 'Navigating and waiting for all network requests to finish' , 'url' , options . url ) ;
273+ }
274+
275+ await page . goto ( options . url , { waitUntil : 'networkidle0' , timeout : options . timeout * 1000 } ) ;
276+
277+ if ( this . config . verboseLogging ) {
278+ this . log . debug ( 'Waiting for download to end' ) ;
279+ }
280+
281+ const startDate = Date . now ( ) ;
282+ while ( Date . now ( ) - startDate <= options . timeout * 1000 ) {
283+ if ( downloadFilePath !== '' ) {
284+ break ;
285+ }
286+ await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
287+ }
288+
289+ if ( downloadFilePath === '' ) {
290+ throw new Error ( `Timeout exceeded while waiting for download to end` ) ;
291+ }
292+
293+ await watcher . close ( ) ;
294+
295+ let filePath = downloadFilePath ;
296+ if ( options . filePath ) {
297+ fs . renameSync ( downloadFilePath , options . filePath ) ;
298+ filePath = options . filePath ;
299+ fs . rmdirSync ( path . dirname ( downloadFilePath ) ) ;
300+ }
301+
302+ return { filePath, fileName : path . basename ( downloadFilePath ) } ;
303+ }
304+
205305 addPageListeners ( page : any ) {
206306 page . on ( 'error' , this . logError ) ;
207307 page . on ( 'pageerror' , this . logPageError ) ;
0 commit comments