@coder/pixel-storybook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/api.d.ts +56 -0
- package/dist/api.js +238 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +102 -0
- package/dist/checkDifferences.d.ts +63 -0
- package/dist/checkDifferences.js +67 -0
- package/dist/compare/compare.d.ts +5 -0
- package/dist/compare/compare.js +80 -0
- package/dist/compare/pixelmatch.d.ts +7 -0
- package/dist/compare/pixelmatch.js +46 -0
- package/dist/compare/utils.d.ts +2 -0
- package/dist/compare/utils.js +18 -0
- package/dist/compare/worker.d.ts +1 -0
- package/dist/compare/worker.js +6 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +61 -0
- package/dist/config.d.ts +3795 -0
- package/dist/config.js +472 -0
- package/dist/configHelper.d.ts +2 -0
- package/dist/configHelper.js +34 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/crawler/storybook.d.ts +51 -0
- package/dist/crawler/storybook.js +317 -0
- package/dist/crawler/utils.d.ts +6 -0
- package/dist/crawler/utils.js +20 -0
- package/dist/createShots.d.ts +30 -0
- package/dist/createShots.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +99 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +186 -0
- package/dist/schemas.d.ts +174 -0
- package/dist/schemas.js +73 -0
- package/dist/shard.d.ts +3 -0
- package/dist/shard.js +17 -0
- package/dist/shots/shots.d.ts +3 -0
- package/dist/shots/shots.js +196 -0
- package/dist/shots/utils.d.ts +33 -0
- package/dist/shots/utils.js +177 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/dist/upload.d.ts +10 -0
- package/dist/upload.js +32 -0
- package/dist/uploadStorybook.d.ts +2 -0
- package/dist/uploadStorybook.js +56 -0
- package/dist/utils.d.ts +50 -0
- package/dist/utils.js +194 -0
- package/package.json +64 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import pixelmatch from 'pixelmatch';
|
|
3
|
+
import { PNG } from 'pngjs';
|
|
4
|
+
import { resizeImage } from './utils.js';
|
|
5
|
+
export const checkThreshold = (threshold, pixelsTotal, pixelDifference) => {
|
|
6
|
+
// Treat theshold as percentage
|
|
7
|
+
if (threshold < 1) {
|
|
8
|
+
return pixelDifference <= pixelsTotal * threshold;
|
|
9
|
+
}
|
|
10
|
+
// Treat threshold as absolute value
|
|
11
|
+
return pixelDifference <= threshold;
|
|
12
|
+
};
|
|
13
|
+
export const runPixelmatchComparison = (threshold, baselineShotPath, currentShotPath, differenceShotPath) => {
|
|
14
|
+
const baselineImageBuffer = readFileSync(baselineShotPath);
|
|
15
|
+
const currentImageBuffer = readFileSync(currentShotPath);
|
|
16
|
+
if (baselineImageBuffer.equals(currentImageBuffer)) {
|
|
17
|
+
return { pixelDifference: 0, pixelDifferencePercentage: 0, isWithinThreshold: true };
|
|
18
|
+
}
|
|
19
|
+
let baselineImage = PNG.sync.read(baselineImageBuffer);
|
|
20
|
+
let currentImage = PNG.sync.read(currentImageBuffer);
|
|
21
|
+
const maxWidth = Math.max(baselineImage.width || 100, currentImage.width || 100);
|
|
22
|
+
const maxHeight = Math.max(baselineImage.height || 100, currentImage.height || 100);
|
|
23
|
+
if (baselineImage.width !== currentImage.width || baselineImage.height !== currentImage.height) {
|
|
24
|
+
baselineImage = resizeImage(baselineImage, maxWidth, maxHeight);
|
|
25
|
+
currentImage = resizeImage(currentImage, maxWidth, maxHeight);
|
|
26
|
+
}
|
|
27
|
+
const differenceImage = new PNG({ width: maxWidth, height: maxHeight });
|
|
28
|
+
const pixelDifference = pixelmatch(baselineImage.data, currentImage.data, differenceImage.data, maxWidth, maxHeight, { threshold: 0 });
|
|
29
|
+
const pixelsTotal = baselineImage.width * baselineImage.height;
|
|
30
|
+
if (pixelDifference > 0 && differenceShotPath) {
|
|
31
|
+
const isWithinThreshold = checkThreshold(threshold, pixelsTotal, pixelDifference);
|
|
32
|
+
if (!isWithinThreshold) {
|
|
33
|
+
writeFileSync(differenceShotPath, PNG.sync.write(differenceImage));
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
pixelDifference,
|
|
37
|
+
pixelDifferencePercentage: pixelDifference / pixelsTotal,
|
|
38
|
+
isWithinThreshold,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
pixelDifference,
|
|
43
|
+
pixelDifferencePercentage: pixelDifference / pixelsTotal,
|
|
44
|
+
isWithinThreshold: true,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
export const resizeImage = (originalImage, width, height) => {
|
|
3
|
+
const newImage = new PNG({
|
|
4
|
+
width,
|
|
5
|
+
height,
|
|
6
|
+
fill: true,
|
|
7
|
+
inputHasAlpha: true,
|
|
8
|
+
});
|
|
9
|
+
for (let x = 0; x < width; x++) {
|
|
10
|
+
for (let y = 0; y < height; y++) {
|
|
11
|
+
// eslint-disable-next-line no-bitwise
|
|
12
|
+
const index = ((width * y + x) << 2) + 3;
|
|
13
|
+
newImage.data[index] = 64;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
PNG.bitblt(originalImage, newImage, 0, 0, originalImage.width, originalImage.height, 0, 0);
|
|
17
|
+
return newImage;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// This file runs inside a worker_threads Worker.
|
|
2
|
+
// It performs the CPU-bound pixelmatch comparison off the main thread.
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { runPixelmatchComparison } from './pixelmatch.js';
|
|
5
|
+
const result = runPixelmatchComparison(workerData.threshold, workerData.baselineShotPath, workerData.currentShotPath, workerData.differenceShotPath);
|
|
6
|
+
parentPort?.postMessage(result);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default number of concurrent shots to take, derived from the number of
|
|
3
|
+
* CPUs the process is allowed to use (respects cgroup CPU quotas).
|
|
4
|
+
*/
|
|
5
|
+
export declare const defaultShotConcurrency: () => number;
|
|
6
|
+
/**
|
|
7
|
+
* Run an array of async tasks with a concurrency limit.
|
|
8
|
+
* Drop-in replacement for `async.mapLimit`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const mapLimit: <T, R>(items: Iterable<T>, limit: number, fn: (item: T) => Promise<R>) => Promise<R[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Retry an async function with exponential backoff.
|
|
13
|
+
* Drop-in replacement for `async.retry`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const retry: <T>(options: {
|
|
16
|
+
times: number;
|
|
17
|
+
interval: (retryCount: number) => number;
|
|
18
|
+
errorFilter: (error: Error) => boolean;
|
|
19
|
+
}, fn: () => Promise<T>) => Promise<T>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { availableParallelism } from 'node:os';
|
|
2
|
+
/**
|
|
3
|
+
* Floor on the auto default. Matches the prior static default so we never
|
|
4
|
+
* regress on small CI runners.
|
|
5
|
+
*/
|
|
6
|
+
const SHOT_CONCURRENCY_AUTO_MIN = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Ceiling on the auto default. Each Playwright BrowserContext costs ~100-
|
|
9
|
+
* 200MB of RSS during a shot. We cap to avoid OOMing on machines with very
|
|
10
|
+
* high CPU counts but proportionally less RAM.
|
|
11
|
+
*/
|
|
12
|
+
const SHOT_CONCURRENCY_AUTO_MAX = 32;
|
|
13
|
+
/**
|
|
14
|
+
* Default number of concurrent shots to take, derived from the number of
|
|
15
|
+
* CPUs the process is allowed to use (respects cgroup CPU quotas).
|
|
16
|
+
*/
|
|
17
|
+
export const defaultShotConcurrency = () => {
|
|
18
|
+
const cpus = availableParallelism();
|
|
19
|
+
return Math.max(SHOT_CONCURRENCY_AUTO_MIN, Math.min(cpus, SHOT_CONCURRENCY_AUTO_MAX));
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Run an array of async tasks with a concurrency limit.
|
|
23
|
+
* Drop-in replacement for `async.mapLimit`.
|
|
24
|
+
*/
|
|
25
|
+
export const mapLimit = async (items, limit, fn) => {
|
|
26
|
+
const results = [];
|
|
27
|
+
const entries = [...items];
|
|
28
|
+
let index = 0;
|
|
29
|
+
const worker = async () => {
|
|
30
|
+
while (index < entries.length) {
|
|
31
|
+
const i = index++;
|
|
32
|
+
results[i] = await fn(entries[i]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const workers = Array.from({ length: Math.min(limit, entries.length) }, () => worker());
|
|
36
|
+
await Promise.all(workers);
|
|
37
|
+
return results;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Retry an async function with exponential backoff.
|
|
41
|
+
* Drop-in replacement for `async.retry`.
|
|
42
|
+
*/
|
|
43
|
+
export const retry = async (options, fn) => {
|
|
44
|
+
let lastError;
|
|
45
|
+
for (let attempt = 0; attempt < options.times; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
if (!options.errorFilter(lastError)) {
|
|
52
|
+
throw lastError;
|
|
53
|
+
}
|
|
54
|
+
if (attempt < options.times - 1) {
|
|
55
|
+
const delay = options.interval(attempt + 1);
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw lastError;
|
|
61
|
+
};
|