UNPKG

@percy/sdk-utils

Version:

Common JavaScript SDK utils

333 lines (307 loc) 11.8 kB
(function() { (function (exports) { 'use strict'; const process = (typeof globalThis !== "undefined" && globalThis.process) || {}; process.env = process.env || {}; process.env.__PERCY_BROWSERIFIED__ = true; // helper to create a version object from a string function toVersion(str) { str || (str = '0.0.0'); return str.split(/\.|-/).reduce((version, part, i) => { let v = parseInt(part, 10); version[i] = isNaN(v) ? part : v; return version; }, { get major() { return this[0] || 0; }, get minor() { return this[1] || 0; }, get patch() { return this[2] || 0; }, get prerelease() { return this[3]; }, get build() { return this[4]; }, toString() { return str; } }); } // private version cache let version = toVersion(); let type; // contains local percy info const info = { // get or set the CLI API address via the environment get address() { return process.env.PERCY_SERVER_ADDRESS || 'http://localhost:5338'; }, set address(addr) { return process.env.PERCY_SERVER_ADDRESS = addr; }, // version information get version() { return version; }, set version(v) { return version = toVersion(v); }, get type() { return type; }, set type(t) { return type = t; } }; // Helper to send a request to the local CLI API async function request(path, options = {}) { let url = path.startsWith('http') ? path : `${info.address}${path}`; let response = await request.fetch(url, options); // maybe parse response body as json if (typeof response.body === 'string' && response.headers['content-type'] === 'application/json') { try { response.body = JSON.parse(response.body); } catch (e) {} } // throw an error if status is not ok if (!(response.status >= 200 && response.status < 300)) { throw Object.assign(new Error(), { message: response.body.error || /* istanbul ignore next: in tests, there's always an error message */ `${response.status} ${response.statusText}`, response }); } return response; } request.post = function post(url, json) { return request(url, { method: 'POST', body: JSON.stringify(json), timeout: 600000 }); }; // environment specific implementation if (process.env.__PERCY_BROWSERIFIED__) { // use window.fetch in browsers const winFetch = window.fetch; request.fetch = async function fetch(url, options) { let response = await winFetch(url, options); return { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), body: await response.text() }; }; } else { // use http.request in node request.fetch = async function fetch(url, options) { let { protocol } = new URL(url); // rollup throws error for -> await ({}) let { default: http } = protocol === 'https:' ? await ({}) : await ({}); return new Promise((resolve, reject) => { http.request(url, options).on('response', response => { let body = ''; response.on('data', chunk => body += chunk.toString()); response.on('end', () => resolve({ status: response.statusCode, statusText: response.statusMessage, headers: response.headers, body })); }).on('error', reject).end(options.body); }); }; } // Used when determining if a message should be logged const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; // Create a small logger util using the specified namespace function logger(namespace) { return Object.keys(LOG_LEVELS).reduce((ns, lvl) => Object.assign(ns, { [lvl]: (...a) => logger.log(namespace, lvl, ...a) }), {}); } Object.assign(logger, { // Set and/or return the local loglevel loglevel: (lvl = logger.loglevel.lvl) => { return logger.loglevel.lvl = lvl || process.env.PERCY_LOGLEVEL || 'info'; }, // Track and send/write logs for the specified namespace and log level // remote should only be false in case of sensitive/self call for errors log: (ns, lvl, msg, meta, remote = true) => { let err = typeof msg !== 'string' && (lvl === 'error' || lvl === 'debug'); // check if the specific level is within the local loglevel range if (LOG_LEVELS[lvl] != null && LOG_LEVELS[lvl] >= LOG_LEVELS[logger.loglevel()]) { let debug = logger.loglevel() === 'debug'; let label = debug ? `percy:${ns}` : 'percy'; // colorize the label when possible for consistency with the CLI logger if (!process.env.__PERCY_BROWSERIFIED__) label = `\u001b[95m${label}\u001b[39m`; msg = `[${label}] ${err && debug && msg.stack || msg}`; if (process.env.__PERCY_BROWSERIFIED__) { // use console[warn|error|log] in browsers console[['warn', 'error'].includes(lvl) ? lvl : 'log'](msg); } else { // use process[stdout|stderr].write in node process[lvl === 'info' ? 'stdout' : 'stderr'].write(msg + '\n'); } if (remote && (lvl === 'error' || debug)) { return request.post('/percy/log', { level: lvl, message: msg, meta }).catch(_ => { logger.log(ns, 'error', 'Could not send logs to cli', meta, false); }); } } } }); // Check if Percy is enabled using the healthcheck endpoint async function isPercyEnabled() { if (info.enabled == null) { let log = logger('utils'); let error; try { let response = await request('/percy/healthcheck'); info.version = response.headers['x-percy-core-version']; info.config = response.body.config; info.build = response.body.build; info.enabled = true; info.type = response.body.type; info.widths = response.body.widths; } catch (e) { info.enabled = false; error = e; } if (info.enabled && info.version.major !== 1) { log.info('Unsupported Percy CLI version, disabling snapshots'); log.debug(`Found version: ${info.version}`); info.enabled = false; } else if (!info.enabled) { log.info('Percy is not running, disabling snapshots'); log.debug(error); } } return info.enabled; } const RETRY_ERROR_CODES = ['ECONNRESET', 'ETIMEDOUT']; async function waitForPercyIdle() { try { return !!(await request('/percy/idle')); } catch (e) { return RETRY_ERROR_CODES.includes(e.code) && waitForPercyIdle(); } } // Fetch and cache the @percy/dom script async function fetchPercyDOM() { if (info.domScript == null) { let response = await request('/percy/dom.js'); info.domScript = response.body; } return info.domScript; } // Post snapshot data to the CLI snapshot endpoint. If the endpoint responds with a build error, // indicate that Percy has been disabled. async function postSnapshot(options, params) { let query = params ? `?${new URLSearchParams(params)}` : ''; return await request.post(`/percy/snapshot${query}`, options).catch(err => { var _err$response; if ((_err$response = err.response) !== null && _err$response !== void 0 && (_err$response = _err$response.body) !== null && _err$response !== void 0 && (_err$response = _err$response.build) !== null && _err$response !== void 0 && _err$response.error) { info.enabled = false; } else { throw err; } }); } // Post snapshot data to the CLI snapshot endpoint. If the endpoint responds with a build error, // indicate that Percy has been disabled. async function postComparison(options, params) { let query = params ? `?${new URLSearchParams(params)}` : ''; return await request.post(`/percy/comparison${query}`, options).catch(err => { var _err$response; if ((_err$response = err.response) !== null && _err$response !== void 0 && (_err$response = _err$response.body) !== null && _err$response !== void 0 && (_err$response = _err$response.build) !== null && _err$response !== void 0 && _err$response.error) { info.enabled = false; } else { throw err; } }); } // Post failed event data to the CLI event endpoint. async function postBuildEvents(options) { return await request.post('/percy/events', options).catch(err => { throw err; }); } // Posts to the local Percy server one or more snapshots to flush. Given no arguments, all snapshots // will be flushed. Does nothing when Percy is not enabled. async function flushSnapshots(options) { if (info.enabled) { // accept one or more snapshot names options && (options = [].concat(options).map(o => typeof o === 'string' ? { name: o } : o)); await request.post('/percy/flush', options); } } // Post screenshot data to the CLI automateScreenshot endpoint. If the endpoint responds with a build error, // indicate that Percy has been disabled. async function captureAutomateScreenshot(options, params) { let query = params ? `?${new URLSearchParams(params)}` : ''; return await request.post(`/percy/automateScreenshot${query}`, options).catch(err => { var _err$response; if ((_err$response = err.response) !== null && _err$response !== void 0 && (_err$response = _err$response.body) !== null && _err$response !== void 0 && (_err$response = _err$response.build) !== null && _err$response !== void 0 && _err$response.error) { info.enabled = false; } else { throw err; } }); } var index = /*#__PURE__*/Object.freeze({ __proto__: null, logger: logger, percy: info, request: request, isPercyEnabled: isPercyEnabled, waitForPercyIdle: waitForPercyIdle, fetchPercyDOM: fetchPercyDOM, postSnapshot: postSnapshot, postComparison: postComparison, flushSnapshots: flushSnapshots, captureAutomateScreenshot: captureAutomateScreenshot, postBuildEvents: postBuildEvents, 'default': index }); exports.captureAutomateScreenshot = captureAutomateScreenshot; exports["default"] = index; exports.fetchPercyDOM = fetchPercyDOM; exports.flushSnapshots = flushSnapshots; exports.isPercyEnabled = isPercyEnabled; exports.logger = logger; exports.percy = info; exports.postBuildEvents = postBuildEvents; exports.postComparison = postComparison; exports.postSnapshot = postSnapshot; exports.request = request; exports.waitForPercyIdle = waitForPercyIdle; Object.defineProperty(exports, '__esModule', { value: true }); })(this.PercySDKUtils = this.PercySDKUtils || {}); }).call(window); if (typeof define === "function" && define.amd) { define("@percy/sdk-utils", [], () => window.PercySDKUtils); } else if (typeof module === "object" && module.exports) { module.exports = window.PercySDKUtils; }