UNPKG

@percy/agent

Version:

An agent process for integrating with Percy.

146 lines (145 loc) 7.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentService = void 0; const bodyParser = require("body-parser"); const cors = require("cors"); const express = require("express"); const os = require("os"); const path = require("path"); const logger_1 = require("../utils/logger"); const agent_service_constants_1 = require("./agent-service-constants"); const build_service_1 = require("./build-service"); const constants_1 = require("./constants"); const process_service_1 = require("./process-service"); const snapshot_service_1 = require("./snapshot-service"); class AgentService { constructor() { this.snapshotService = null; this.publicDirectory = `${__dirname}/../../dist/public`; this.snapshotCreationPromises = []; this.snapshotConfig = {}; this.server = null; this.buildId = null; this.app = express(); this.app.use(cors()); this.app.use(bodyParser.urlencoded({ extended: true })); this.app.use(bodyParser.json({ limit: '50mb' })); this.app.use(express.static(this.publicDirectory)); this.app.post(agent_service_constants_1.SNAPSHOT_PATH, async (request, response) => { try { await this.handleSnapshot.call(this, request, response); } catch (error) { logger_1.default.error(logger_1.addLogDate(`${error.name} ${error.message}`)); logger_1.default.debug(logger_1.addLogDate(error)); return response.json({ success: false }); } }); this.app.post(agent_service_constants_1.STOP_PATH, this.handleStop.bind(this)); this.app.get(agent_service_constants_1.HEALTHCHECK_PATH, this.handleHealthCheck.bind(this)); this.buildService = new build_service_1.default(); } async start(configuration) { this.snapshotConfig = configuration.snapshot; this.buildId = await this.buildService.create(); if (this.buildId !== null) { this.server = this.app.listen(configuration.agent.port); this.snapshotService = new snapshot_service_1.default(this.buildId, configuration.agent['asset-discovery']); await this.snapshotService.assetDiscoveryService.setup(); return; } await this.stop(); } async stop() { logger_1.default.info('stopping percy...'); logger_1.default.info(`waiting for ${this.snapshotCreationPromises.length} snapshots to complete...`); await Promise.all(this.snapshotCreationPromises); logger_1.default.info('done.'); if (this.snapshotService) { await this.snapshotService.assetDiscoveryService.teardown(); } await this.buildService.finalize(); if (this.server) { await this.server.close(); } } async handleSnapshot(request, response) { logger_1.profile('agentService.handleSnapshot'); // truncate domSnapshot for the logs if it's very large const rootURL = request.body.url; let domSnapshotLog = request.body.domSnapshot; if (domSnapshotLog.length > constants_1.default.MAX_LOG_LENGTH) { domSnapshotLog = domSnapshotLog.substring(0, constants_1.default.MAX_LOG_LENGTH); domSnapshotLog += `[truncated at ${constants_1.default.MAX_LOG_LENGTH}]`; } const snapshotLog = path.join(os.tmpdir(), `percy.${Date.now()}.log`); const snapshotLogger = logger_1.createFileLogger(snapshotLog); snapshotLogger.debug('handling snapshot:'); snapshotLogger.debug(`-> headers: ${JSON.stringify(request.headers)}`); snapshotLogger.debug(`-> name: ${request.body.name}`); snapshotLogger.debug(`-> url: ${request.body.url}`); snapshotLogger.debug(`-> clientInfo: ${request.body.clientInfo}`); snapshotLogger.debug(`-> environmentInfo: ${request.body.environmentInfo}`); snapshotLogger.debug(`-> domSnapshot: ${domSnapshotLog}`); if (!this.snapshotService) { return response.json({ success: false }); } // trim the string of whitespace and concat per-snapshot CSS with the globally specified CSS const percySpecificCSS = this.snapshotConfig['percy-css'].concat(request.body.percyCSS || '').trim(); const hasWidths = !!request.body.widths && request.body.widths.length; const snapshotOptions = { percyCSS: percySpecificCSS, requestHeaders: request.body.requestHeaders, widths: hasWidths ? request.body.widths : this.snapshotConfig.widths, enableJavaScript: request.body.enableJavaScript != null ? request.body.enableJavaScript : this.snapshotConfig['enable-javascript'], minHeight: request.body.minHeight || this.snapshotConfig['min-height'], }; snapshotLogger.debug(`-> widths: ${snapshotOptions.widths}`); snapshotLogger.debug(`-> minHeight: ${snapshotOptions.minHeight}`); snapshotLogger.debug(`-> enableJavaScript: ${snapshotOptions.enableJavaScript}`); snapshotLogger.debug(`-> requestHeaders: ${snapshotOptions.requestHeaders}`); snapshotLogger.debug(`-> percyCSS: ${snapshotOptions.percyCSS}`); let domSnapshot = request.body.domSnapshot; if (domSnapshot.length > constants_1.default.MAX_FILE_SIZE_BYTES) { logger_1.default.info(`snapshot skipped[max_file_size_exceeded]: '${request.body.name}'`); return response.json({ success: true }); } // tslint:disable-next-line let resolve, reject; const deferred = new Promise((...args) => [resolve, reject] = args); this.snapshotCreationPromises.push(deferred); let resources = await this.snapshotService.buildResources(rootURL, domSnapshot, snapshotOptions, snapshotLogger); const percyCSSFileName = `percy-specific.${Date.now()}.css`; // Inject the link to the percy specific css if the option is passed // This must be done _AFTER_ asset discovery, or you risk their server // serving a response for this CSS we're injecting into the DOM if (snapshotOptions.percyCSS) { const cssLink = `<link data-percy-specific-css rel="stylesheet" href="/${percyCSSFileName}" />`; domSnapshot = domSnapshot.replace(/(<\/body>)(?!.*\1)/is, cssLink + '$&'); } resources = resources.concat(this.snapshotService.buildRootResource(rootURL, domSnapshot), // @ts-ignore we won't write anything if css is not is passed this.snapshotService.buildPercyCSSResource(rootURL, percyCSSFileName, snapshotOptions.percyCSS, snapshotLogger), this.snapshotService.buildLogResource(snapshotLog)); this.snapshotService.create(request.body.name, resources, snapshotOptions, request.body.clientInfo, request.body.environmentInfo).then(resolve).catch(reject); logger_1.default.info(`snapshot taken: '${request.body.name}'`); logger_1.profile('agentService.handleSnapshot'); return response.json({ success: true }); } async handleStop(_, response) { await this.stop(); new process_service_1.default().cleanup(); return response.json({ success: true }); } async handleHealthCheck(_, response) { return response.json({ success: true, build: { number: this.buildService.buildNumber, url: this.buildService.buildUrl, }, }); } } exports.AgentService = AgentService;