source/index.js
import {packageOf} from '@tech_query/node-toolkit';
import {resolve} from 'url';
import WebServer from 'koapache';
import {watch} from 'chokidar';
import {toString} from 'qrcode';
import promisify from 'promisify-node';
const QRCode = promisify( toString );
const { env } = process, config = (packageOf('./test') || '').meta;
const NPM_command = env.npm_lifecycle_script;
var server, browser, page;
/**
* Wrapper of `Puppeteer` class
*/
export default class PuppeteerBrowser {
/**
* @protected
*
* @param {string} [root] - Root path of the static site
*
* @return {Object} Server information
*/
static async getServer(root) {
return server || (
server = await (new WebServer(root || '.')).workerHost()
);
}
/**
* @type {string}
*/
static get browserName() {
return (env.npm_config_PUPPETEER_BROWSER || 'chrome').trim();
}
/**
* @type {string}
*/
static get moduleName() {
return 'puppeteer' + (map => {
for (let name in map)
if (name === this.browserName) return map[name];
return '';
})({
chrome: '',
firefox: '-fx',
IE: '-ie'
});
}
/**
* @param {Object} [options] - https://pptr.dev/#?product=Puppeteer&version=v1.5.0&show=api-puppeteerlaunchoptions
*
* @return {Browser}
*/
static async launch(options) {
const Puppeteer = (await import( this.moduleName )).default;
return await Puppeteer.launch( options );
}
/**
* @return {string} https://pptr.dev/#?product=Puppeteer&version=v1.5.0&show=api-puppeteerexecutablepath
*/
static executablePath() {
return env['npm_config_' + this.browserName];
}
/**
* @protected
*
* @param {boolean} [visible] - Browser visibility
* (Visible mode will run slowly for seeing clearly)
* @return {Browser}
*/
static async getBrowser(visible) {
if ( browser ) return browser;
visible = (visible != null) ?
visible : NPM_command.includes('--inspect');
browser = await this.launch({
executablePath: this.executablePath(),
headless: ! visible,
slowMo: visible ? 100 : 0
});
return browser.on('disconnected', () => browser = page = null);
}
/**
* After files changed, the page will be focused & reloaded
*
* @param {string} path - Directory to watch recursively
* @param {function} onChange - Call on files changed
*
* @return {FSWatcher}
*/
static watch(path, onChange) {
var listen;
async function refresh() {
await onChange();
if ( page ) {
await page.bringToFront();
await page.reload();
console.info(`[ Reload ] ${page.url()}`);
}
listen = false;
}
return watch( path ).on('change', () => {
if (! listen) {
listen = true;
process.nextTick( refresh );
}
});
}
/**
* @param {?string} root - Root path to start Web server, default to be `process.cwd()`
* @param {?string} path - Path to open Web page
* @param {function} [fileChange] - Do something between files changed & page reload
* (Browser will be visible)
* @return {Page} Resolve after `DOMContentLoaded` event fired
*/
static async getPage(root, path, fileChange) {
if ( page ) return page;
path = path || '.';
fileChange = (fileChange instanceof Function) ? fileChange : null;
const server = path.indexOf('http') && await this.getServer( root );
const URI = resolve(`http://${server.address}:${server.port}/`, path);
if ( fileChange ) console.info(await QRCode( URI ));
page = await (await this.getBrowser( fileChange )).newPage();
await page.on('close', () => page = null).goto(server ? URI : path, {
waitUntil: 'domcontentloaded'
});
if ( fileChange )
this.watch(config.directories.lib || root, fileChange);
return page;
}
}