| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 | 'use strict';const os = require('os');const path = require('path');const fs = require('fs');function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }const path__default = /*#__PURE__*/_interopDefaultLegacy(path);const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);function fileExists(filePath) {    return new Promise((resolve) => {        fs__default['default'].access(filePath, (err) => resolve(!err));    });}function readFile(filePath) {    return new Promise((resolve, reject) => {        fs__default['default'].readFile(filePath, 'utf-8', (err, data) => {            if (err) {                reject(err);            }            else {                resolve(data);            }        });    });}function readFileBuffer(filePath) {    return new Promise((resolve, reject) => {        fs__default['default'].readFile(filePath, (err, data) => {            if (err) {                reject(err);            }            else {                resolve(data);            }        });    });}function writeFile(filePath, data) {    return new Promise((resolve, reject) => {        fs__default['default'].writeFile(filePath, data, (err) => {            if (err) {                reject(err);            }            else {                resolve();            }        });    });}function mkDir(filePath) {    return new Promise((resolve) => {        fs__default['default'].mkdir(filePath, () => {            resolve();        });    });}function rmDir(filePath) {    return new Promise((resolve) => {        fs__default['default'].rmdir(filePath, () => {            resolve();        });    });}async function emptyDir(dir) {    const files = await readDir(dir);    const promises = files.map(async (fileName) => {        const filePath = path__default['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) => {        fs__default['default'].readdir(dir, (err, files) => {            if (err) {                resolve([]);            }            else {                resolve(files);            }        });    });}async function isFile(itemPath) {    return new Promise((resolve) => {        fs__default['default'].stat(itemPath, (err, stat) => {            if (err) {                resolve(false);            }            else {                resolve(stat.isFile());            }        });    });}async function unlink(filePath) {    return new Promise((resolve) => {        fs__default['default'].unlink(filePath, () => {            resolve();        });    });}class ScreenshotConnector {    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 = path.join(this.rootDir, this.screenshotDirName);        this.imagesDir = path.join(this.screenshotDir, this.imagesDirName);        this.buildsDir = path.join(this.screenshotDir, this.buildsDirName);        this.masterBuildFilePath = path.join(this.buildsDir, this.masterBuildFileName);        this.screenshotCacheFilePath = path.join(this.cacheDir, this.screenshotCacheFileName);        this.currentBuildDir = path.join(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() {        let masterBuild = null;        try {            masterBuild = JSON.parse(await readFile(this.masterBuildFilePath));        }        catch (e) { }        return masterBuild;    }    async completeBuild(masterBuild) {        const filePaths = (await readDir(this.currentBuildDir))            .map((f) => path.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: screenshots,            };        }        const results = {            appNamespace: this.appNamespace,            masterBuild: masterBuild,            currentBuild: {                id: this.buildId,                message: this.buildMessage,                author: this.buildAuthor,                url: this.buildUrl,                previewUrl: this.previewUrl,                appNamespace: this.appNamespace,                timestamp: this.buildTimestamp,                screenshots: 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 = path.join(this.cacheDir, jsonpFileName);                const jsonpExists = await fileExists(jsonFilePath);                if (!jsonpExists) {                    const imageFilePath = path.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) {                    // no need to cache identical matches                    return;                }                const existingItem = screenshotCache.items.find((i) => i.key === diff.cacheKey);                if (existingItem) {                    // already have this cached, but update its timestamp                    existingItem.ts = this.buildTimestamp;                }                else {                    // add this item to the cache                    screenshotCache.items.push({                        key: diff.cacheKey,                        ts: this.buildTimestamp,                        mp: diff.mismatchedPixels,                    });                }            });        }        // sort so the newest items are on top        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;        });        // keep only the most recent items        screenshotCache.items = screenshotCache.items.slice(0, 1000);        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: 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;        });    }}/** * Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar * Forward-slash paths can be used in Windows as long as they're not * extended-length paths and don't contain any non-ascii characters. * This was created since the path methods in Node.js outputs \\ paths on Windows. * @param path the Windows-based path to convert * @returns the converted path */const normalizePath = (path) => {    if (typeof path !== 'string') {        throw new Error(`invalid path to normalize`);    }    path = normalizeSlashes(path.trim());    const components = pathComponents(path, getRootLength(path));    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 &&        path.includes('/') &&        !secondPart.startsWith('.') &&        !secondPart.startsWith('@')) {        return './' + normalized;    }    return normalized;};const normalizeSlashes = (path) => path.replace(backslashRegExp, '/');const altDirectorySeparator = '\\';const urlSchemeSeparator = '://';const backslashRegExp = /\\/g;const 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;};const getRootLength = (path) => {    const rootLength = getEncodedRootLength(path);    return rootLength < 0 ? ~rootLength : rootLength;};const getEncodedRootLength = (path) => {    if (!path)        return 0;    const ch0 = path.charCodeAt(0);    // POSIX or UNC    if (ch0 === 47 /* CharacterCodes.slash */ || ch0 === 92 /* CharacterCodes.backslash */) {        if (path.charCodeAt(1) !== ch0)            return 1; // POSIX: "/" (or non-normalized "\")        const p1 = path.indexOf(ch0 === 47 /* CharacterCodes.slash */ ? '/' : altDirectorySeparator, 2);        if (p1 < 0)            return path.length; // UNC: "//server" or "\\server"        return p1 + 1; // UNC: "//server/" or "\\server\"    }    // DOS    if (isVolumeCharacter(ch0) && path.charCodeAt(1) === 58 /* CharacterCodes.colon */) {        const ch2 = path.charCodeAt(2);        if (ch2 === 47 /* CharacterCodes.slash */ || ch2 === 92 /* CharacterCodes.backslash */)            return 3; // DOS: "c:/" or "c:\"        if (path.length === 2)            return 2; // DOS: "c:" (but not "c:d")    }    // URL    const schemeEnd = path.indexOf(urlSchemeSeparator);    if (schemeEnd !== -1) {        const authorityStart = schemeEnd + urlSchemeSeparator.length;        const authorityEnd = path.indexOf('/', authorityStart);        if (authorityEnd !== -1) {            // URL: "file:///", "file://server/", "file://server/path"            // For local "file" URLs, include the leading DOS volume (if present).            // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a            // special case interpreted as "the machine from which the URL is being interpreted".            const scheme = path.slice(0, schemeEnd);            const authority = path.slice(authorityStart, authorityEnd);            if (scheme === 'file' &&                (authority === '' || authority === 'localhost') &&                isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {                const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);                if (volumeSeparatorEnd !== -1) {                    if (path.charCodeAt(volumeSeparatorEnd) === 47 /* CharacterCodes.slash */) {                        // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"                        return ~(volumeSeparatorEnd + 1);                    }                    if (volumeSeparatorEnd === path.length) {                        // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"                        // but not "file:///c:d" or "file:///c%3ad"                        return ~volumeSeparatorEnd;                    }                }            }            return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"        }        return ~path.length; // URL: "file://server", "http://server"    }    // relative    return 0;};const isVolumeCharacter = (charCode) => (charCode >= 97 /* CharacterCodes.a */ && charCode <= 122 /* CharacterCodes.z */) ||    (charCode >= 65 /* CharacterCodes.A */ && charCode <= 90 /* CharacterCodes.Z */);const getFileUrlVolumeSeparatorEnd = (url, start) => {    const ch0 = url.charCodeAt(start);    if (ch0 === 58 /* CharacterCodes.colon */)        return start + 1;    if (ch0 === 37 /* CharacterCodes.percent */ && url.charCodeAt(start + 1) === 51 /* CharacterCodes._3 */) {        const ch2 = url.charCodeAt(start + 2);        if (ch2 === 97 /* CharacterCodes.a */ || ch2 === 65 /* CharacterCodes.A */)            return start + 3;    }    return -1;};const pathComponents = (path, rootLength) => {    const root = path.substring(0, rootLength);    const rest = path.substring(rootLength).split('/');    const restLen = rest.length;    if (restLen > 0 && !rest[restLen - 1]) {        rest.pop();    }    return [root, ...rest];};class ScreenshotLocalConnector 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 = path.join(this.packageDir, 'screenshot', 'compare');        const appSrcUrl = normalizePath(path.relative(this.screenshotDir, compareAppSourceDir));        const imagesUrl = normalizePath(path.relative(this.screenshotDir, this.imagesDir));        const jsonpUrl = normalizePath(path.relative(this.screenshotDir, this.cacheDir));        const compareAppHtml = createLocalCompareApp(this.appNamespace, appSrcUrl, imagesUrl, jsonpUrl, results.masterBuild, results.currentBuild);        const compareAppFileName = 'compare.html';        const compareAppFilePath = path.join(this.screenshotDir, compareAppFileName);        await writeFile(compareAppFilePath, compareAppHtml);        const gitIgnorePath = path.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>`;}exports.ScreenshotConnector = ScreenshotConnector;exports.ScreenshotLocalConnector = ScreenshotLocalConnector;
 |