@percy/sdk-utils
Version:
Common JavaScript SDK utils
333 lines (307 loc) • 11.8 kB
JavaScript
(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;
}