UNPKG

@stencil/core

Version:

A Compiler for Web Components and Progressive Web Apps

681 lines (673 loc) • 23.9 kB
/* Stencil Screenshot v4.22.2 | MIT Licensed | https://stenciljs.com */ "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/screenshot/index.ts var screenshot_exports = {}; __export(screenshot_exports, { ScreenshotConnector: () => ScreenshotConnector, ScreenshotLocalConnector: () => ScreenshotLocalConnector }); module.exports = __toCommonJS(screenshot_exports); // src/screenshot/connector-base.ts var import_os = require("os"); var import_path2 = require("path"); // src/screenshot/screenshot-fs.ts var import_fs = __toESM(require("fs")); var import_path = __toESM(require("path")); function fileExists(filePath) { return new Promise((resolve) => { import_fs.default.access(filePath, (err2) => resolve(!err2)); }); } function readFile(filePath) { return new Promise((resolve, reject) => { import_fs.default.readFile(filePath, "utf-8", (err2, data) => { if (err2) { reject(err2); } else { resolve(data); } }); }); } function readFileBuffer(filePath) { return new Promise((resolve, reject) => { import_fs.default.readFile(filePath, (err2, data) => { if (err2) { reject(err2); } else { resolve(data); } }); }); } function writeFile(filePath, data) { return new Promise((resolve, reject) => { import_fs.default.writeFile(filePath, data, (err2) => { if (err2) { reject(err2); } else { resolve(); } }); }); } function mkDir(filePath) { return new Promise((resolve) => { import_fs.default.mkdir(filePath, () => { resolve(); }); }); } function rmDir(filePath) { return new Promise((resolve) => { import_fs.default.rmdir(filePath, () => { resolve(); }); }); } async function emptyDir(dir) { const files = await readDir(dir); const promises = files.map(async (fileName) => { const filePath = import_path.default.join(dir, fileName); const isDirFile = await isFile(filePath); if (isDirFile) { await unlink(filePath); } }); await Promise.all(promises); } async function readDir(dir) { return new Promise((resolve) => { import_fs.default.readdir(dir, (err2, files) => { if (err2) { resolve([]); } else { resolve(files); } }); }); } async function isFile(itemPath) { return new Promise((resolve) => { import_fs.default.stat(itemPath, (err2, stat) => { if (err2) { resolve(false); } else { resolve(stat.isFile()); } }); }); } async function unlink(filePath) { return new Promise((resolve) => { import_fs.default.unlink(filePath, () => { resolve(); }); }); } // src/screenshot/connector-base.ts var ScreenshotConnector = class { constructor() { this.screenshotDirName = "screenshot"; this.imagesDirName = "images"; this.buildsDirName = "builds"; this.masterBuildFileName = "master.json"; this.screenshotCacheFileName = "screenshot-cache.json"; } async initBuild(opts) { this.logger = opts.logger; this.buildId = opts.buildId; this.buildMessage = opts.buildMessage || ""; this.buildAuthor = opts.buildAuthor; this.buildUrl = opts.buildUrl; this.previewUrl = opts.previewUrl; this.buildTimestamp = typeof opts.buildTimestamp === "number" ? opts.buildTimestamp : Date.now(); this.cacheDir = opts.cacheDir; this.packageDir = opts.packageDir; this.rootDir = opts.rootDir; this.appNamespace = opts.appNamespace; this.waitBeforeScreenshot = opts.waitBeforeScreenshot; this.pixelmatchModulePath = opts.pixelmatchModulePath; if (!opts.logger) { throw new Error(`logger option required`); } if (typeof opts.buildId !== "string") { throw new Error(`buildId option required`); } if (typeof opts.cacheDir !== "string") { throw new Error(`cacheDir option required`); } if (typeof opts.packageDir !== "string") { throw new Error(`packageDir option required`); } if (typeof opts.rootDir !== "string") { throw new Error(`rootDir option required`); } this.updateMaster = !!opts.updateMaster; this.allowableMismatchedPixels = opts.allowableMismatchedPixels; this.allowableMismatchedRatio = opts.allowableMismatchedRatio; this.pixelmatchThreshold = opts.pixelmatchThreshold; this.logger.debug(`screenshot build: ${this.buildId}, ${this.buildMessage}, updateMaster: ${this.updateMaster}`); this.logger.debug( `screenshot, allowableMismatchedPixels: ${this.allowableMismatchedPixels}, allowableMismatchedRatio: ${this.allowableMismatchedRatio}, pixelmatchThreshold: ${this.pixelmatchThreshold}` ); if (typeof opts.screenshotDirName === "string") { this.screenshotDirName = opts.screenshotDirName; } if (typeof opts.imagesDirName === "string") { this.imagesDirName = opts.imagesDirName; } if (typeof opts.buildsDirName === "string") { this.buildsDirName = opts.buildsDirName; } this.screenshotDir = (0, import_path2.join)(this.rootDir, this.screenshotDirName); this.imagesDir = (0, import_path2.join)(this.screenshotDir, this.imagesDirName); this.buildsDir = (0, import_path2.join)(this.screenshotDir, this.buildsDirName); this.masterBuildFilePath = (0, import_path2.join)(this.buildsDir, this.masterBuildFileName); this.screenshotCacheFilePath = (0, import_path2.join)(this.cacheDir, this.screenshotCacheFileName); this.currentBuildDir = (0, import_path2.join)((0, import_os.tmpdir)(), "screenshot-build-" + this.buildId); this.logger.debug(`screenshotDirPath: ${this.screenshotDir}`); this.logger.debug(`imagesDirPath: ${this.imagesDir}`); this.logger.debug(`buildsDirPath: ${this.buildsDir}`); this.logger.debug(`currentBuildDir: ${this.currentBuildDir}`); this.logger.debug(`cacheDir: ${this.cacheDir}`); await mkDir(this.screenshotDir); await Promise.all([ mkDir(this.imagesDir), mkDir(this.buildsDir), mkDir(this.currentBuildDir), mkDir(this.cacheDir) ]); } async pullMasterBuild() { } async getMasterBuild() { try { const masterBuild = JSON.parse(await readFile(this.masterBuildFilePath)); return masterBuild; } catch (e) { } return null; } async completeBuild(masterBuild) { const filePaths = (await readDir(this.currentBuildDir)).map((f) => (0, import_path2.join)(this.currentBuildDir, f)).filter((f) => f.endsWith(".json")); const screenshots = await Promise.all(filePaths.map(async (f) => JSON.parse(await readFile(f)))); this.sortScreenshots(screenshots); if (!masterBuild) { masterBuild = { id: this.buildId, message: this.buildMessage, author: this.buildAuthor, url: this.buildUrl, previewUrl: this.previewUrl, appNamespace: this.appNamespace, timestamp: this.buildTimestamp, screenshots }; } const results = { appNamespace: this.appNamespace, masterBuild, currentBuild: { id: this.buildId, message: this.buildMessage, author: this.buildAuthor, url: this.buildUrl, previewUrl: this.previewUrl, appNamespace: this.appNamespace, timestamp: this.buildTimestamp, screenshots }, compare: { id: `${masterBuild.id}-${this.buildId}`, a: { id: masterBuild.id, message: masterBuild.message, author: masterBuild.author, url: masterBuild.url, previewUrl: masterBuild.previewUrl }, b: { id: this.buildId, message: this.buildMessage, author: this.buildAuthor, url: this.buildUrl, previewUrl: this.previewUrl }, url: null, appNamespace: this.appNamespace, timestamp: this.buildTimestamp, diffs: [] } }; results.currentBuild.screenshots.forEach((screenshot) => { screenshot.diff.device = screenshot.diff.device || screenshot.diff.userAgent; results.compare.diffs.push(screenshot.diff); delete screenshot.diff; }); this.sortCompares(results.compare.diffs); await emptyDir(this.currentBuildDir); await rmDir(this.currentBuildDir); return results; } async publishBuild(results) { return results; } async generateJsonpDataUris(build) { if (build && Array.isArray(build.screenshots)) { for (let i = 0; i < build.screenshots.length; i++) { const screenshot = build.screenshots[i]; const jsonpFileName = `screenshot_${screenshot.image}.js`; const jsonFilePath = (0, import_path2.join)(this.cacheDir, jsonpFileName); const jsonpExists = await fileExists(jsonFilePath); if (!jsonpExists) { const imageFilePath = (0, import_path2.join)(this.imagesDir, screenshot.image); const imageBuf = await readFileBuffer(imageFilePath); const jsonpContent = `loadScreenshot("${screenshot.image}","data:image/png;base64,${imageBuf.toString( "base64" )}");`; await writeFile(jsonFilePath, jsonpContent); } } } } async getScreenshotCache() { return null; } async updateScreenshotCache(screenshotCache, buildResults) { screenshotCache = screenshotCache || {}; screenshotCache.timestamp = this.buildTimestamp; screenshotCache.lastBuildId = this.buildId; screenshotCache.size = 0; screenshotCache.items = screenshotCache.items || []; if (buildResults && buildResults.compare && Array.isArray(buildResults.compare.diffs)) { buildResults.compare.diffs.forEach((diff) => { if (typeof diff.cacheKey !== "string") { return; } if (diff.imageA === diff.imageB) { return; } const existingItem = screenshotCache.items.find((i) => i.key === diff.cacheKey); if (existingItem) { existingItem.ts = this.buildTimestamp; } else { screenshotCache.items.push({ key: diff.cacheKey, ts: this.buildTimestamp, mp: diff.mismatchedPixels }); } }); } screenshotCache.items.sort((a, b) => { if (a.ts > b.ts) return -1; if (a.ts < b.ts) return 1; if (a.mp > b.mp) return -1; if (a.mp < b.mp) return 1; return 0; }); screenshotCache.items = screenshotCache.items.slice(0, 1e3); screenshotCache.size = screenshotCache.items.length; return screenshotCache; } toJson(masterBuild, screenshotCache) { const masterScreenshots = {}; if (masterBuild && Array.isArray(masterBuild.screenshots)) { masterBuild.screenshots.forEach((masterScreenshot) => { masterScreenshots[masterScreenshot.id] = masterScreenshot.image; }); } const mismatchCache = {}; if (screenshotCache && Array.isArray(screenshotCache.items)) { screenshotCache.items.forEach((cacheItem) => { mismatchCache[cacheItem.key] = cacheItem.mp; }); } const screenshotBuild = { buildId: this.buildId, rootDir: this.rootDir, screenshotDir: this.screenshotDir, imagesDir: this.imagesDir, buildsDir: this.buildsDir, masterScreenshots, cache: mismatchCache, currentBuildDir: this.currentBuildDir, updateMaster: this.updateMaster, allowableMismatchedPixels: this.allowableMismatchedPixels, allowableMismatchedRatio: this.allowableMismatchedRatio, pixelmatchThreshold: this.pixelmatchThreshold, timeoutBeforeScreenshot: this.waitBeforeScreenshot, pixelmatchModulePath: this.pixelmatchModulePath }; return JSON.stringify(screenshotBuild); } sortScreenshots(screenshots) { return screenshots.sort((a, b) => { if (a.desc && b.desc) { if (a.desc.toLowerCase() < b.desc.toLowerCase()) return -1; if (a.desc.toLowerCase() > b.desc.toLowerCase()) return 1; } if (a.device && b.device) { if (a.device.toLowerCase() < b.device.toLowerCase()) return -1; if (a.device.toLowerCase() > b.device.toLowerCase()) return 1; } if (a.userAgent && b.userAgent) { if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase()) return -1; if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase()) return 1; } if (a.width < b.width) return -1; if (a.width > b.width) return 1; if (a.height < b.height) return -1; if (a.height > b.height) return 1; if (a.id < b.id) return -1; if (a.id > b.id) return 1; return 0; }); } sortCompares(compares) { return compares.sort((a, b) => { if (a.allowableMismatchedPixels > b.allowableMismatchedPixels) return -1; if (a.allowableMismatchedPixels < b.allowableMismatchedPixels) return 1; if (a.allowableMismatchedRatio > b.allowableMismatchedRatio) return -1; if (a.allowableMismatchedRatio < b.allowableMismatchedRatio) return 1; if (a.desc && b.desc) { if (a.desc.toLowerCase() < b.desc.toLowerCase()) return -1; if (a.desc.toLowerCase() > b.desc.toLowerCase()) return 1; } if (a.device && b.device) { if (a.device.toLowerCase() < b.device.toLowerCase()) return -1; if (a.device.toLowerCase() > b.device.toLowerCase()) return 1; } if (a.userAgent && b.userAgent) { if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase()) return -1; if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase()) return 1; } if (a.width < b.width) return -1; if (a.width > b.width) return 1; if (a.height < b.height) return -1; if (a.height > b.height) return 1; if (a.id < b.id) return -1; if (a.id > b.id) return 1; return 0; }); } }; // src/utils/path.ts var normalizePath = (path2, relativize = true) => { if (typeof path2 !== "string") { throw new Error(`invalid path to normalize`); } path2 = normalizeSlashes(path2.trim()); const components = pathComponents(path2, getRootLength(path2)); const reducedComponents = reducePathComponents(components); const rootPart = reducedComponents[0]; const secondPart = reducedComponents[1]; const normalized = rootPart + reducedComponents.slice(1).join("/"); if (normalized === "") { return "."; } if (rootPart === "" && secondPart && path2.includes("/") && !secondPart.startsWith(".") && !secondPart.startsWith("@") && relativize) { return "./" + normalized; } return normalized; }; var normalizeSlashes = (path2) => path2.replace(backslashRegExp, "/"); var altDirectorySeparator = "\\"; var urlSchemeSeparator = "://"; var backslashRegExp = /\\/g; var reducePathComponents = (components) => { if (!Array.isArray(components) || components.length === 0) { return []; } const reduced = [components[0]]; for (let i = 1; i < components.length; i++) { const component = components[i]; if (!component) continue; if (component === ".") continue; if (component === "..") { if (reduced.length > 1) { if (reduced[reduced.length - 1] !== "..") { reduced.pop(); continue; } } else if (reduced[0]) continue; } reduced.push(component); } return reduced; }; var getRootLength = (path2) => { const rootLength = getEncodedRootLength(path2); return rootLength < 0 ? ~rootLength : rootLength; }; var getEncodedRootLength = (path2) => { if (!path2) return 0; const ch0 = path2.charCodeAt(0); if (ch0 === 47 /* slash */ || ch0 === 92 /* backslash */) { if (path2.charCodeAt(1) !== ch0) return 1; const p1 = path2.indexOf(ch0 === 47 /* slash */ ? "/" : altDirectorySeparator, 2); if (p1 < 0) return path2.length; return p1 + 1; } if (isVolumeCharacter(ch0) && path2.charCodeAt(1) === 58 /* colon */) { const ch2 = path2.charCodeAt(2); if (ch2 === 47 /* slash */ || ch2 === 92 /* backslash */) return 3; if (path2.length === 2) return 2; } const schemeEnd = path2.indexOf(urlSchemeSeparator); if (schemeEnd !== -1) { const authorityStart = schemeEnd + urlSchemeSeparator.length; const authorityEnd = path2.indexOf("/", authorityStart); if (authorityEnd !== -1) { const scheme = path2.slice(0, schemeEnd); const authority = path2.slice(authorityStart, authorityEnd); if (scheme === "file" && (authority === "" || authority === "localhost") && isVolumeCharacter(path2.charCodeAt(authorityEnd + 1))) { const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path2, authorityEnd + 2); if (volumeSeparatorEnd !== -1) { if (path2.charCodeAt(volumeSeparatorEnd) === 47 /* slash */) { return ~(volumeSeparatorEnd + 1); } if (volumeSeparatorEnd === path2.length) { return ~volumeSeparatorEnd; } } } return ~(authorityEnd + 1); } return ~path2.length; } return 0; }; var isVolumeCharacter = (charCode) => charCode >= 97 /* a */ && charCode <= 122 /* z */ || charCode >= 65 /* A */ && charCode <= 90 /* Z */; var getFileUrlVolumeSeparatorEnd = (url, start) => { const ch0 = url.charCodeAt(start); if (ch0 === 58 /* colon */) return start + 1; if (ch0 === 37 /* percent */ && url.charCodeAt(start + 1) === 51 /* _3 */) { const ch2 = url.charCodeAt(start + 2); if (ch2 === 97 /* a */ || ch2 === 65 /* A */) return start + 3; } return -1; }; var pathComponents = (path2, rootLength) => { const root = path2.substring(0, rootLength); const rest = path2.substring(rootLength).split("/"); const restLen = rest.length; if (restLen > 0 && !rest[restLen - 1]) { rest.pop(); } return [root, ...rest]; }; // src/utils/result.ts var result_exports = {}; __export(result_exports, { err: () => err, map: () => map, ok: () => ok, unwrap: () => unwrap, unwrapErr: () => unwrapErr }); var ok = (value) => ({ isOk: true, isErr: false, value }); var err = (value) => ({ isOk: false, isErr: true, value }); function map(result, fn) { if (result.isOk) { const val = fn(result.value); if (val instanceof Promise) { return val.then((newVal) => ok(newVal)); } else { return ok(val); } } if (result.isErr) { const value = result.value; return err(value); } throw "should never get here"; } var unwrap = (result) => { if (result.isOk) { return result.value; } else { throw result.value; } }; var unwrapErr = (result) => { if (result.isErr) { return result.value; } else { throw result.value; } }; // src/screenshot/connector-local.ts var import_path4 = require("path"); var ScreenshotLocalConnector = class extends ScreenshotConnector { async publishBuild(results) { if (this.updateMaster || !results.masterBuild) { results.masterBuild = { id: "master", message: "Master", appNamespace: this.appNamespace, timestamp: Date.now(), screenshots: [] }; } results.currentBuild.screenshots.forEach((currentScreenshot) => { const masterHasScreenshot = results.masterBuild.screenshots.some((masterScreenshot) => { return currentScreenshot.id === masterScreenshot.id; }); if (!masterHasScreenshot) { results.masterBuild.screenshots.push(Object.assign({}, currentScreenshot)); } }); this.sortScreenshots(results.masterBuild.screenshots); await writeFile(this.masterBuildFilePath, JSON.stringify(results.masterBuild, null, 2)); await this.generateJsonpDataUris(results.currentBuild); const compareAppSourceDir = (0, import_path4.join)(this.packageDir, "screenshot", "compare"); const appSrcUrl = normalizePath((0, import_path4.relative)(this.screenshotDir, compareAppSourceDir)); const imagesUrl = normalizePath((0, import_path4.relative)(this.screenshotDir, this.imagesDir)); const jsonpUrl = normalizePath((0, import_path4.relative)(this.screenshotDir, this.cacheDir)); const compareAppHtml = createLocalCompareApp( this.appNamespace, appSrcUrl, imagesUrl, jsonpUrl, results.masterBuild, results.currentBuild ); const compareAppFileName = "compare.html"; const compareAppFilePath = (0, import_path4.join)(this.screenshotDir, compareAppFileName); await writeFile(compareAppFilePath, compareAppHtml); const gitIgnorePath = (0, import_path4.join)(this.screenshotDir, ".gitignore"); const gitIgnoreExists = await fileExists(gitIgnorePath); if (!gitIgnoreExists) { const content = [this.imagesDirName, this.buildsDirName, compareAppFileName]; await writeFile(gitIgnorePath, content.join("\n")); } const url = new URL(`file://${compareAppFilePath}`); results.compare.url = url.href; return results; } async getScreenshotCache() { let screenshotCache = null; try { screenshotCache = JSON.parse(await readFile(this.screenshotCacheFilePath)); } catch (e) { } return screenshotCache; } async updateScreenshotCache(cache, buildResults) { cache = await super.updateScreenshotCache(cache, buildResults); await writeFile(this.screenshotCacheFilePath, JSON.stringify(cache, null, 2)); return cache; } }; function createLocalCompareApp(namespace, appSrcUrl, imagesUrl, jsonpUrl, a, b) { return `<!doctype html> <html dir="ltr" lang="en"> <head> <meta charset="utf-8"> <title>Local ${namespace || ""} - Stencil Screenshot Visual Diff</title> <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta http-equiv="x-ua-compatible" content="IE=Edge"> <link href="${appSrcUrl}/build/app.css" rel="stylesheet"> <script type="module" src="${appSrcUrl}/build/app.esm.js"></script> <script nomodule src="${appSrcUrl}/build/app.js"></script> <link rel="icon" type="image/x-icon" href="${appSrcUrl}/assets/favicon.ico"> </head> <body> <script> (function() { var app = document.createElement('screenshot-compare'); app.appSrcUrl = '${appSrcUrl}'; app.imagesUrl = '${imagesUrl}/'; app.jsonpUrl = '${jsonpUrl}/'; app.a = ${JSON.stringify(a)}; app.b = ${JSON.stringify(b)}; document.body.appendChild(app); })(); </script> </body> </html>`; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ScreenshotConnector, ScreenshotLocalConnector });