@percy/agent
Version:
An agent process for integrating with Percy.
137 lines (136 loc) • 5.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const crypto = require("crypto");
const fs = require("fs");
const globby = require("globby");
const image_size_1 = require("image-size");
const os = require("os");
const path = require("path");
const configuration_1 = require("../configuration/configuration");
const configuration_2 = require("../utils/configuration");
const logger_1 = require("../utils/logger");
const build_service_1 = require("./build-service");
const percy_client_service_1 = require("./percy-client-service");
const ALLOWED_IMAGE_TYPES = /\.(png|jpg|jpeg)$/i;
class ImageSnapshotService extends percy_client_service_1.default {
constructor(configuration) {
super();
this.buildService = new build_service_1.default();
this.configuration = configuration || configuration_1.DEFAULT_CONFIGURATION['image-snapshots'];
}
get buildId() {
return this.buildService.buildId;
}
makeLocalCopy(imagePath) {
logger_1.default.debug(`Making local copy of image: ${imagePath}`);
const buffer = fs.readFileSync(path.resolve(this.configuration.path, imagePath));
const sha = crypto.createHash('sha256').update(buffer).digest('hex');
const filename = path.join(os.tmpdir(), sha);
if (!fs.existsSync(filename)) {
fs.writeFileSync(filename, buffer);
}
else {
logger_1.default.debug(`Skipping file copy [already_copied]: ${imagePath}`);
}
return filename;
}
buildResources(imagePath, width, height) {
const { name, ext } = path.parse(imagePath);
const localCopy = this.makeLocalCopy(imagePath);
const imageUrl = `/${encodeURIComponent(imagePath)}`;
const mimetype = ext === '.png' ? 'image/png' : 'image/jpeg';
const sha = path.basename(localCopy);
const rootResource = this.percyClient.makeResource({
isRoot: true,
resourceUrl: `/${encodeURIComponent(name)}`,
mimetype: 'text/html',
content: `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${imagePath}</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; font-size: 0; }
html, body { width: 100%; }
img { max-width: 100%; }
</style>
</head>
<body>
<img src="${imageUrl}" width="${width}px" height="${height}px"/>
</body>
</html>
`,
});
const imgResource = this.percyClient.makeResource({
resourceUrl: imageUrl,
localPath: localCopy,
mimetype,
sha,
});
return [rootResource, imgResource];
}
async createSnapshot(name, resources, width, height) {
return this.percyClient.createSnapshot(this.buildId, resources, {
name,
// clamp between 10px - 2000px
widths: [Math.max(10, Math.min(width, 2000))],
minimumHeight: Math.max(10, Math.min(height, 2000)),
clientInfo: 'percy-upload',
}).then(async (response) => {
await this.percyClient.uploadMissingResources(this.buildId, response, resources);
return response;
}).then(async (response) => {
const snapshotId = response.body.data.id;
logger_1.profile('-> imageSnapshotService.finalizeSnapshot');
await this.percyClient.finalizeSnapshot(snapshotId);
logger_1.profile('-> imageSnapshotService.finalizeSnapshot', { snapshotId });
return response;
}).catch(logger_1.logError);
}
async snapshotAll({ dry = false } = {}) {
const globs = configuration_2.parseGlobs(this.configuration.files);
const ignore = configuration_2.parseGlobs(this.configuration.ignore);
const paths = (await globby(globs, { cwd: this.configuration.path, ignore })).sort();
let error;
if (!paths.length) {
logger_1.default.error(`no matching files found in '${this.configuration.path}''`);
return process.exit(1);
}
if (dry) {
console.log(paths.join('\n'));
return;
}
await this.buildService.create();
logger_1.default.debug('uploading snapshots of static images');
try {
// wait for snapshots in parallel
await Promise.all(paths.reduce((promises, pathname) => {
logger_1.default.debug(`handling snapshot: '${pathname}'`);
// only snapshot supported images
if (!pathname.match(ALLOWED_IMAGE_TYPES)) {
logger_1.default.info(`skipping unsupported image type: '${pathname}'`);
return promises;
}
// @ts-ignore - if dimensions are undefined, the library throws an error
const { width, height } = image_size_1.imageSize(path.resolve(this.configuration.path, pathname));
const resources = this.buildResources(pathname, width, height);
const snapshotPromise = this.createSnapshot(pathname, resources, width, height);
logger_1.default.info(`snapshot uploaded: '${pathname}'`);
promises.push(snapshotPromise);
return promises;
}, []));
}
catch (err) {
error = err;
logger_1.logError(err);
}
// finalize build
await this.buildService.finalize();
// if an error occurred, exit with non-zero
if (error) {
process.exit(1);
}
}
}
exports.default = ImageSnapshotService;