/** * Sitespeed.io - How speedy is your site? (https://www.sitespeed.io) * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog * and other contributors * Released under the Apache 2.0 License */ 'use strict'; /*eslint no-process-exit:0*/ var fs = require('fs-extra'), fileHelper = require('./util/fileHelpers'), defaultConfig = require('../conf/defaultConfig'), EOL = require('os').EOL, validUrl = require('valid-url'), nomnom = require('nomnom'); var permissionsBits = parseInt('777', 8); var checkPermissionSynch = function(mode, mask) { return ((mode & permissionsBits) & mask) === mask; }; var validatePathOption = function(optionName, path) { if (!fs.existsSync(path)) { return '--' + optionName + ': \'' + path + '\' can\'t be found'; } }; var isJsonString = function(str) { try { JSON.parse(str); } catch (e) { return false; } return true; }; var cli = nomnom.help( 'sitespeed.io is a tool that helps you analyze your website performance and show you what you should optimize, more info at https://www.sitespeed.io.' + EOL + 'To collect timings in Chrome you need to install the ChromeDriver. Firefox works out of the box. Example:' + EOL + '$ sitespeed.io -u https://www.sitespeed.io -b chrome,firefox' ).options({ url: { abbr: 'u', metavar: '', help: 'The start url that will be used when crawling.', callback: function(url) { if (!validUrl.isWebUri(url)) { return '--url: \'' + url + '\' is not a valid url (you need to include protocol)'; } } }, file: { abbr: 'f', metavar: '', help: 'The path to a plain text file with one URL on each row. Each URL will be analyzed.', callback: function(path) { return validatePathOption('file', path); } }, sites: { metavar: '', list: true, help: 'The path to a plain text file with one URL on each row. You can use the parameter multiple times to point out many files', callback: function(path) { return validatePathOption('sites', path); }, transform: function(path) { return fileHelper.getFileAsArray(path); } }, version: { flag: true, abbr: 'V', help: 'Display the sitespeed.io version.', callback: function() { return require('../package.json').version; } }, silent: { flag: true, help: 'Only output info in the logs, not to the console.' }, verbose: { flag: true, abbr: 'v', help: 'Enable verbose logging.' }, noColor: { flag: true, default: false, help: 'Don\'t use colors in console output.' }, deep: { abbr: 'd', metavar: '', default: defaultConfig.deep, help: 'How deep to crawl.', callback: function(deep) { if (isNaN(parseInt(deep))) { return '--deep: You must specify an integer of how deep you want to crawl'; } } }, containInPath: { abbr: 'c', metavar: '', help: 'Only crawl URLs that contains this in the path.' }, skip: { abbr: 's', metavar: '', help: 'Do not crawl pages that contains this in the path.' }, threads: { abbr: 't', metavar: '', default: defaultConfig.threads, help: 'The number of threads/processes that will analyze pages.', callback: function(threads) { var int = parseInt(threads, 10); if (isNaN(int)) { return '--threads: You must specify an integer of how many processes/threads that will analyze your page'; } else if (int <= 0) { return '--threads: You must specify a positive integer'; } } }, name: { metavar: '', help: 'Give your test a name, it will be added to all HTML pages.' }, memory: { metavar: '', default: defaultConfig.memory, help: 'How much memory the Java processed will have (in mb).' }, resultBaseDir: { abbr: 'r', metavar: '', default: defaultConfig.resultBaseDir, help: 'The result base directory, the base dir where the result ends up.', callback: function(path) { if (!fs.existsSync(path)) { try { fs.mkdirsSync(path); } catch (e) { return '--resultBaseDir: Couldn\'t create the result base dir (' + e.code + ')'; } } } }, outputFolderName: { help: 'Default the folder name is a date of format yyyy-mm-dd-HH-MM-ss' }, suppressDomainFolder: { help: 'Do not use the domain folder in the output directory', flag: true }, userAgent: { metavar: '', default: defaultConfig.userAgent, help: 'The full User Agent string, default is Chrome for MacOSX. [userAgent|ipad|iphone].' }, viewPort: { metavar: '', default: defaultConfig.viewPort, help: 'The view port, the page viewport size WidthxHeight like 400x300.' }, yslow: { abbr: 'y', metavar: '', default: defaultConfig.yslow, help: 'The compiled YSlow file. Use this if you have your own rules.', callback: function(path) { return validatePathOption('yslow', path); } }, headless: { default: defaultConfig.headless, help: 'Choose which backend to use for headless [phantomjs|slimerjs]' }, ruleSet: { metavar: '', default: defaultConfig.ruleSet, help: 'Which ruleset to use.' }, limitFile: { metavar: '', help: 'The path to the limit configuration file.', callback: function(path) { return validatePathOption('limitFile', path); } }, basicAuth: { metavar: '', help: 'Basic auth user & password.' }, browser: { abbr: 'b', metavar: '', help: 'Choose which browser to use to collect timing data. Use multiple browsers in a comma separated list (firefox|chrome|headless)', callback: function(browsers) { var b = browsers.split(','), invalidBrowsers = b.filter(function(browser) { return defaultConfig.supportedBrowsers.indexOf(browser.toLowerCase()) < 0; }); if (invalidBrowsers.length > 0) { return '--browser: You specified a browser that is not supported:' + invalidBrowsers; } } }, connection: { default: 'cable', help: 'Limit the speed by simulating connection types. Choose between ' + defaultConfig.connection }, waitScript: { help: 'Supply a javascript that decides when a browser run is finished. Use it to fetch timings happening after the loadEventEnd.', default: defaultConfig.waitScript }, customScripts: { help: 'The path to an extra script folder with scripts that will be executed in the browser. See https://www.sitespeed.io/documentation/browsers/#custom-metrics' }, seleniumServer: { metavar: 'URL', help: 'Configure the path to the Selenium server when fetching timings using browsers. If not configured the supplied NodeJS/Selenium version is used.', default: undefined }, btConfig: { metavar: '', help: 'Additional BrowserTime JSON configuration as a file', callback: function(path) { return validatePathOption('btConfig', path); }, transform: function(path) { return fileHelper.getFileAsJSON(path); } }, profile: { metavar: '', choices: ['desktop', 'mobile'], default: defaultConfig.profile, help: 'Choose between testing for desktop or mobile. Testing for desktop will use desktop rules & user agents and vice verca.' }, no: { abbr: 'n', metavar: '', default: defaultConfig.no, help: 'The number of times you should test each URL when fetching timing metrics. Default is ' + defaultConfig.no + ' times.', callback: function(n) { var int = parseInt(n, 10); if (isNaN(int)) { return '--no: You must specify an integer of how many times you want to test one URL'; } else if (int <= 0) { return '--no: You must specify a positive integer of how many times you want to test one URL'; } } }, screenshot: { flag: true, help: 'Take screenshots for each page (using the configured view port).' }, junit: { flag: true, help: 'Create JUnit output to the console.' }, tap: { flag: true, help: 'Create TAP output to the console.' }, skipTest: { metavar: '', help: 'A comma separated list of rules to skip when generating JUnit/TAP/budget output.' }, testData: { default: defaultConfig.testData, help: 'Choose which data to send test when generating TAP/JUnit output or testing a budget. Default is all available [rules,page,timings,wpt,gpsi]' }, budget: { metavar: '', help: 'A file containing the web perf budget rules. See https://www.sitespeed.io/documentation/performance-budget/', callback: function(path) { return validatePathOption('budget', path); }, transform: function(path) { return fileHelper.getFileAsJSON(path); } }, maxPagesToTest: { abbr: 'm', metavar: '', help: 'The max number of pages to test. Default is no limit.' }, storeJson: { flag: true, help: 'Store all collected data as JSON.' }, proxy: { abbr: 'p', metavar: '', help: 'http://proxy.soulgalore.com:80' }, cdns: { metavar: '', list: true, help: 'A comma separated list of additional CDNs.' }, postTasksDir: { metavar: '', help: 'The directory where you have your extra post tasks.', callback: function(path) { return validatePathOption('postTasksDir', path); } }, boxes: { metavar: '', list: true, help: 'The boxes showed on site summary page, see https://www.sitespeed.io/documentation/configuration/#configure-boxes-on-summary-page' }, columns: { abbr: 'c', metavar: '', list: true, help: 'The columns showed on detailed page summary table, see https://www.sitespeed.io/documentation/configuration/#configure-columns-on-pages-page' }, configFile: { metavar: '', help: 'The path to a sitespeed.io config.json file, if it exists all other input parameters will be overridden.' }, // TODO How to override existing aggregators: { metavar: '', help: 'The path to a directory with extra aggregators.' }, // TODO maybe collectors are overkill collectors: { metavar: '', help: 'The path to a directory with extra collectors.' }, graphiteHost: { metavar: '', help: 'The Graphite host.' }, graphitePort: { metavar: '', default: defaultConfig.graphitePort, help: 'The Graphite port.' }, graphiteNamespace: { metavar: '', default: defaultConfig.graphiteNamespace, help: 'The namespace of the data sent to Graphite.' }, graphiteData: { default: defaultConfig.graphiteData, help: 'Choose which data to send to Graphite by a comma separated list. Default all data is sent. [summary,rules,pagemetrics,timings,requests,domains]' }, graphiteUseQueryParameters: { flag: true, help: 'Choose if you want to use query paramaters from the URL in the Graphite keys or not' }, graphiteUseNewDomainKeyStructure: { flag: true, help: 'Use the updated domain section when sending data to Graphite "http.www.sitespeed.io" to "http.www_sitespeed_io" (issue #651)' }, gpsiKey: { help: 'Your Google API Key, configure it to also fetch data from Google Page Speed Insights.' }, noYslow: { flag: true, help: 'Set to true to turn off collecting metrics using YSlow.' }, html: { flag: true, default: true, help: 'Create HTML reports. Default to true. Set no-html to disable HTML reports.' }, assetPath: { hidden: true, default: '' }, wptConfig: { metavar: '', help: 'WebPageTest configuration, see https://github.com/marcelduran/webpagetest-api runTest method ', callback: function(path) { return validatePathOption('wptConfig', path); }, transform: function(path) { return fileHelper.getFileAsJSON(path); } }, wptScript: { metavar: '', help: 'WebPageTest scripting. Every occurance of {{{URL}}} will be replaced with the real URL.', callback: function(path) { return validatePathOption('wptScript', path); }, transform: function(path) { return fileHelper.getFileAsString(path); } }, wptCustomMetrics: { metavar: '', help: 'Fetch metrics from your page using Javascript', callback: function(path) { return validatePathOption('wptCustomMetrics', path); }, transform: function(path) { return fileHelper.getFileAsString(path); } }, wptHost: { metavar: '', help: 'The domain of your WebPageTest instance.' }, wptKey: { metavar: '', help: 'The API key if running on webpagetest on the public instances.' }, requestHeaders: { metavar: '|
', type: 'string', help: 'Any request headers to use, a file or a header string with JSON form of {\"name\":\"value\",\"name2\":\"value\"}. Not supported for WPT & GPSI.', callback: function(path) { if (!fs.existsSync(path) && !isJsonString(path)) { return '--requestHeaders: \'' + path + '\' is not a file or not a valid JSON header string!'; } }, transform: function(path) { if (fs.existsSync(path)) { return fileHelper.getFileAsJSON(path); } else { return JSON.parse(path); } } }, postURL: { metavar: '', help: 'The full URL where the result JSON will be sent by POST. Warning: Testing many pages can make the result JSON massive.', callback: function(url) { if (!validUrl.isWebUri(url)) { return '--postURL: \'' + url + '\' is not a valid url (you need to include protocol)'; } } }, processJson: { metavar: '', help: 'Pass the path to a result JSON that will be processed again. Use this to reconfigure what to show in the HTML.', hidden: true, transform: function(path) { return fileHelper.getFileAsJSON(path); } }, phantomjsPath: { metavar: '', help: 'The full path to the phantomjs binary, to override the supplied version', callback: function(path) { var isValid = false; try { var stats = fs.statSync(path); if (stats.isFile() && checkPermissionSynch(stats.mode, 1)) { isValid = true; } } catch (e) { if (!(e.code === 'ENOENT')) { throw e; } } if (!isValid) { return '--phantomjsPath: \'' + path + '\' is not an executable file!'; } } } }).parse(); if ((!cli.url) && (!cli.file) && (!cli.sites) && (!cli.configFile)) { console.log('You must specify either a URL to test, a file with URL(s) or a config file'); console.log(nomnom.getUsage()); process.exit(1); } if (cli.file) { cli.urls = fileHelper.getFileAsArray(cli.file); // are all URL(s) valid? var valid = true; cli.urls.forEach(function(url) { if (!validUrl.isWebUri(url)) { console.log(url + ' is not a valid url (you need to include the protocol)'); valid = false; } }); if(!valid) { console.log('Fix the URL(s) before you continue'); process.exit(1); } } else if (cli.sites) { var validSites = true; cli.sites.forEach(function(file) { file.forEach(function(url) { if (!validUrl.isWebUri(url)) { console.log(url + ' is not a valid url (you need to include the protocol)'); validSites = false; } }); }); if(!validSites) { console.log('Fix the URL(s) before you continue'); process.exit(1); } } if (cli.btConfig && (cli.btConfig.useProxy !== undefined)) { console.log('The useProxy property of btConfig is deprecated. ' + 'Please set btConfig.noProxy = true instead to disable the proxy.'); if (cli.btConfig.useProxy === false) { cli.btConfig.noProxy = true; } delete cli.btConfig.useProxy; } module.exports = cli;