#!/usr/bin/env node /** * Copyright 2017 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const util = require('util'); const fs = require('fs'); const path = require('path'); const puppeteer = require('..'); const DEVICES_URL = 'https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/HEAD/front_end/emulated_devices/module.json'; const template = `/** * Copyright 2017 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ module.exports = %s; for (const device of module.exports) module.exports[device.name] = device; `; const help = `Usage: node ${path.basename(__filename)} [-u ] -u, --url The URL to load devices descriptor from. If not set, devices will be fetched from the tip-of-tree of DevTools frontend. -h, --help Show this help message Fetch Chrome DevTools front-end emulation devices from given URL, convert them to puppeteer devices and save to the . `; const argv = require('minimist')(process.argv.slice(2), { alias: { u: 'url', h: 'help' }, }); if (argv.help) { console.log(help); return; } const url = argv.url || DEVICES_URL; const outputPath = argv._[0]; if (!outputPath) { console.log('ERROR: output file name is missing. Use --help for help.'); return; } main(url); async function main(url) { const browser = await puppeteer.launch(); const chromeVersion = (await browser.version()).split('/').pop(); await browser.close(); console.log('GET ' + url); const text = await httpGET(url); let json = null; try { json = JSON.parse(text); } catch (error) { console.error(`FAILED: error parsing response - ${error.message}`); return; } const devicePayloads = json.extensions .filter((extension) => extension.type === 'emulated-device') .map((extension) => extension.device); let devices = []; for (const payload of devicePayloads) { let names = []; if (payload.title === 'iPhone 6/7/8') names = ['iPhone 6', 'iPhone 7', 'iPhone 8']; else if (payload.title === 'iPhone 6/7/8 Plus') names = ['iPhone 6 Plus', 'iPhone 7 Plus', 'iPhone 8 Plus']; else if (payload.title === 'iPhone 5/SE') names = ['iPhone 5', 'iPhone SE']; else names = [payload.title]; for (const name of names) { const device = createDevice(chromeVersion, name, payload, false); const landscape = createDevice(chromeVersion, name, payload, true); devices.push(device); if ( landscape.viewport.width !== device.viewport.width || landscape.viewport.height !== device.viewport.height ) devices.push(landscape); } } devices = devices.filter((device) => device.viewport.isMobile); devices.sort((a, b) => a.name.localeCompare(b.name)); // Use single-quotes instead of double-quotes to conform with codestyle. const serialized = JSON.stringify(devices, null, 2) .replace(/'/g, `\\'`) .replace(/"/g, `'`); const result = util.format(template, serialized); fs.writeFileSync(outputPath, result, 'utf8'); } /** * @param {string} chromeVersion * @param {string} deviceName * @param {*} descriptor * @param {boolean} landscape * @returns {!Object} */ function createDevice(chromeVersion, deviceName, descriptor, landscape) { const devicePayload = loadFromJSONV1(descriptor); const viewportPayload = landscape ? devicePayload.horizontal : devicePayload.vertical; return { name: deviceName + (landscape ? ' landscape' : ''), userAgent: devicePayload.userAgent.includes('%s') ? util.format(devicePayload.userAgent, chromeVersion) : devicePayload.userAgent, viewport: { width: viewportPayload.width, height: viewportPayload.height, deviceScaleFactor: devicePayload.deviceScaleFactor, isMobile: devicePayload.capabilities.includes('mobile'), hasTouch: devicePayload.capabilities.includes('touch'), isLandscape: landscape || false, }, }; } /** * @param {*} json * @returns {?Object} */ function loadFromJSONV1(json) { /** * @param {*} object * @param {string} key * @param {string} type * @param {*=} defaultValue * @returns {*} */ function parseValue(object, key, type, defaultValue) { if ( typeof object !== 'object' || object === null || !object.hasOwnProperty(key) ) { if (typeof defaultValue !== 'undefined') return defaultValue; throw new Error( "Emulated device is missing required property '" + key + "'" ); } const value = object[key]; if (typeof value !== type || value === null) throw new Error( "Emulated device property '" + key + "' has wrong type '" + typeof value + "'" ); return value; } /** * @param {*} object * @param {string} key * @returns {number} */ function parseIntValue(object, key) { const value = /** @type {number} */ (parseValue(object, key, 'number')); if (value !== Math.abs(value)) throw new Error("Emulated device value '" + key + "' must be integer"); return value; } /** * @param {*} json * @returns {!{width: number, height: number}} */ function parseOrientation(json) { const result = {}; const minDeviceSize = 50; const maxDeviceSize = 9999; result.width = parseIntValue(json, 'width'); if ( result.width < 0 || result.width > maxDeviceSize || result.width < minDeviceSize ) throw new Error('Emulated device has wrong width: ' + result.width); result.height = parseIntValue(json, 'height'); if ( result.height < 0 || result.height > maxDeviceSize || result.height < minDeviceSize ) throw new Error('Emulated device has wrong height: ' + result.height); return /** @type {!{width: number, height: number}} */ (result); } const result = {}; result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); result.userAgent = /** @type {string} */ ( parseValue(json, 'user-agent', 'string') ); const capabilities = parseValue(json, 'capabilities', 'object', []); if (!Array.isArray(capabilities)) throw new Error('Emulated device capabilities must be an array'); result.capabilities = []; for (let i = 0; i < capabilities.length; ++i) { if (typeof capabilities[i] !== 'string') throw new Error('Emulated device capability must be a string'); result.capabilities.push(capabilities[i]); } result.deviceScaleFactor = /** @type {number} */ ( parseValue(json['screen'], 'device-pixel-ratio', 'number') ); if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) throw new Error( 'Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor ); result.vertical = parseOrientation( parseValue(json['screen'], 'vertical', 'object') ); result.horizontal = parseOrientation( parseValue(json['screen'], 'horizontal', 'object') ); return result; } /** * @param {url} * @returns {!Promise} */ function httpGET(url) { let fulfill, reject; const promise = new Promise((res, rej) => { fulfill = res; reject = rej; }); const driver = url.startsWith('https://') ? require('https') : require('http'); const request = driver.get(url, (response) => { let data = ''; response.setEncoding('utf8'); response.on('data', (chunk) => (data += chunk)); response.on('end', () => fulfill(data)); response.on('error', reject); }); request.on('error', reject); return promise; }