import { promisify } from 'util' ; import { parse as parseUrl } from 'url' ; import path = require ( 'path' ) ; import fs = require ( 'fs' ) ; import os = require ( 'os' ) ; import { EventEmitter } from 'events' ; import pMemoize = require ( 'p-memoize' ) ; import filenamify = require ( 'filenamify' ) ; import unusedFilename from 'unused-filename' ; import arrayUniq = require ( 'array-uniq' ) ; import arrayDiffer = require ( 'array-differ' ) ; import dateFns = require ( 'date-fns' ) ; import getResolutions = require ( 'get-res' ) ; import logSymbols = require ( 'log-symbols' ) ; import makeDir = require ( 'make-dir' ) ; import captureWebsite = require ( 'capture-website' ) ; import viewportList = require ( 'viewport-list' ) ; import template = require ( 'lodash.template' ) ; import plur = require ( 'plur' ) ; import filenamifyUrl = require ( 'filenamify-url' ) ; import pMap = require ( 'p-map' ) ; type Mutable < ObjectType > = { - readonly [ KeyType in keyof ObjectType ] : ObjectType [ KeyType ] ; } ; const writeFile = promisify ( fs . writeFile ) ; const cpuCount = os . cpus ( ) . length ; export interface Options { default readonly delay ? : number ; default readonly timeout ? : number ; default readonly crop ? : boolean ; /**
    Apply custom CSS to the webpage. Specify some CSS or the path to a CSS file.
    */ readonly css ? : string ; /**
    Apply custom JavaScript to the webpage. Specify some JavaScript or the path to a file.
    */ readonly script ? : string ; default readonly cookies ? : ReadonlyArray < string | Record < string , string > > ; default readonly filename ? : string ; default readonly incrementalName ? : boolean ; /**
    Capture a specific DOM element matching a CSS selector.
    */ readonly selector ? : string ; default readonly hide ? : readonly string [ ] ; /**
    Username for authenticating with HTTP auth.
    */ readonly username ? : string ; /**
    Password for authenticating with HTTP auth.
    */ readonly password ? : string ; default readonly scale ? : number ; default readonly format ? : 'png' | 'jpg' | 'jpeg' ; /**
    Custom user agent.
    */ readonly userAgent ? : string ; default readonly headers ? : Record < string , string > ; default readonly transparent ? : boolean ; default readonly darkMode ? : boolean ; default readonly launchOptions ? : captureWebsite . Options [ 'launchOptions' ] ; } /**
A page to screenshot added in  Pageres . src .
 export interface Source { /**
    URL or local path to a website to screenshot. Can also be a data URI.
    */ readonly url : string ; /**
    Size of screenshot. Uses `<width>x<height>` notation or a keyword.
    */ readonly sizes : string [ ] ; /**
    Options which will take precedence over the ones set in the constructor.
    */ readonly options ? : Options ; } /**
A destination directory set in  Pageres . dest .
 export type Destination = string ; export interface Viewport { readonly url : string ; readonly sizes : string [ ] ; readonly keywords : string [ ] ; } interface Stats { urls : number ; sizes : number ; screenshots : number ; } /**
Buffer data representing a screenshot. Includes the filename from the template in  Options . filename .
 export type Screenshot = Buffer & { filename : string ; } ; const getResolutionsMemoized = pMemoize ( getResolutions ) ; const viewportListMemoized = pMemoize ( viewportList ) ; /**
Capture screenshots of websites in various resolutions. A good way to make sure your websites are responsive. It's speedy and generates 100 screenshots from 10 different websites in just over a minute. It can also be used to render SVG images.
*/ export default class Pageres extends EventEmitter { private readonly options : Mutable < Options > ; private stats : Stats ; private readonly items : Screenshot [ ] ; private readonly sizes : string [ ] ; private readonly urls : string [ ] ; private readonly _source : Source [ ] ; private _destination : Destination ; constructor ( options = { } ) { super ( ) ; this . setMaxListeners ( Number . POSITIVE_INFINITY ) ; this . options = { ... options } ; this . options . filename = this . options . filename ?? '<%= url %>-<%= size %><%= crop %>' ; this . options . format = this . options . format ?? 'png' ; this . options . incrementalName = this . options . incrementalName ?? false ; this . options . launchOptions = this . options . launchOptions ?? { } ; this . stats = { } as Stats ; this . items = [ ] ; this . sizes = [ ] ; this . urls = [ ] ; this . _source = [ ] ; this . _destination = '' ; } returns src ( ) ; param url param sizes param options example src ( url , sizes , options ? ) ; src ( url ? , sizes ? , options ? ) { if ( url === undefined ) { return this . _source ; } if ( ! ( typeof url === 'string' && url . length > 0 ) ) { throw new TypeError ( 'URL required' ) ; } if ( ! ( Array . isArray ( sizes ) && sizes . length > 0 ) ) { throw new TypeError ( 'Sizes required' ) ; } this . _source . push ( { url , sizes , options } ) ; return this ; } /**
    Get the destination directory.
    */ dest ( ) ; example dest ( directory ) ; dest ( directory ? ) { if ( directory === undefined ) { return this . _destination ; } if ( ! ( typeof directory === 'string' && directory . length > 0 ) ) { throw new TypeError ( 'Directory required' ) ; } this . _destination = directory ; return this ; } returns example async run ( ) { await Promise . all ( this . src ( ) . map ( async ( source ) : Promise < void > => { const options = { ... this . options , ... source . options } ; const sizes = arrayUniq ( source . sizes . filter ( size => /^\d{2,4}x\d{2,4}$/i . test ( size ) ) ) ; const keywords = arrayDiffer ( source . sizes , sizes ) ; this . urls . push ( source . url ) ; if ( sizes . length === 0 && keywords . includes ( 'w3counter' ) ) { return this . resolution ( source . url , options ) ; } if ( keywords . length > 0 ) { return this . viewport ( { url : source . url , sizes , keywords } , options ) ; } const screenshots = await pMap ( sizes , async ( size ) : Promise < Screenshot > => { this . sizes . push ( size ) ; return this . create ( source . url , size , options ) ; } , { concurrency : cpuCount * 2 } ) ; this . items . push ( ... screenshots ) ; return undefined ; } ) ) ; this . stats . urls = arrayUniq ( this . urls ) . length ; this . stats . sizes = arrayUniq ( this . sizes ) . length ; this . stats . screenshots = this . items . length ; if ( ! this . dest ( ) ) { return this . items ; } await this . save ( this . items ) ; return this . items ; } example successMessage ( ) { const { screenshots , sizes , urls } = this . stats ; const words = { screenshots : plur ( 'screenshot' , screenshots ) , sizes : plur ( 'size' , sizes ) , urls : plur ( 'url' , urls ) } ; console . log ( `\n${ logSymbols . success } Generated ${ screenshots } ${ words . screenshots } from ${ urls } ${ words . urls } and ${ sizes } ${ words . sizes }` ) ; } private async resolution ( url , options ) { for ( const item of await getResolutionsMemoized ( ) as Array < { item : string ; } > ) { this . sizes . push ( item . item ) ; this . items . push ( await this . create ( url , item . item , options ) ) ; } } private async viewport ( viewport , options ) { for ( const item of await viewportListMemoized ( viewport . keywords ) as Array < { size : string ; } > ) { this . sizes . push ( item . size ) ; viewport . sizes . push ( item . size ) ; } for ( const size of arrayUniq ( viewport . sizes ) ) { this . items . push ( await this . create ( viewport . url , size , options ) ) ; } } private async save ( screenshots ) { await Promise . all ( screenshots . map ( async ( screenshot ) => { await makeDir ( this . dest ( ) ) ; const dest = path . join ( this . dest ( ) , screenshot . filename ) ; await writeFile ( dest , screenshot ) ; } ) ) ; } private async create ( url , size , options ) { const basename = fs . existsSync ( url ) ? path . basename ( url ) : url ; let hash = parseUrl ( url ) . hash ?? '' ; if ( /^#!?\/?$/ . test ( hash ) ) { hash = '' ; } const [ width , height ] = size . split ( 'x' ) ; const filenameTemplate = template ( `${ options . filename ! }.${ options . format ! }` ) ; const now = Date . now ( ) ; let filename = filenameTemplate ( { crop : options . crop ? '-cropped' : '' , date : dateFns . format ( now , 'yyyy-MM-dd' ) , time : dateFns . format ( now , 'HH-mm-ss' ) , size , width , height , url : `${ filenamifyUrl ( basename ) }${ filenamify ( hash ) }` } ) ; if ( options . incrementalName ) { filename = unusedFilename . sync ( filename ) ; } const finalOptions = { width : Number ( width ) , height : Number ( height ) , delay : options . delay , timeout : options . timeout , fullPage : ! options . crop , styles : options . css && [ options . css ] , defaultBackground : ! options . transparent , scripts : options . script && [ options . script ] , cookies : options . cookies , element : options . selector , hideElements : options . hide , scaleFactor : options . scale === undefined ? 1 : options . scale , type : options . format === 'jpg' ? 'jpeg' : 'png' , userAgent : options . userAgent , headers : options . headers , darkMode : options . darkMode , launchOptions : options . launchOptions } ; if ( options . username && options . password ) { finalOptions . authentication = { username : options . username , password : options . password } ; } const screenshot = await captureWebsite . buffer ( url , finalOptions ) as any ; screenshot . filename = filename ; return screenshot ; } } module . exports = Pageres ; module . exports . default = Pageres ;