stitch2 0.22.0 → 0.22.2
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/dist/check.js +10 -3
- package/dist/report.js +99 -4
- package/package.json +1 -1
package/dist/check.js
CHANGED
|
@@ -47,12 +47,19 @@ const auditOpts = { comfortable: config.comfortableTarget, durations: (() => { t
|
|
|
47
47
|
const server = await startServer(0);
|
|
48
48
|
/** The tallest render, in CSS px: a long landing page is shot whole (the canvas draws frames up to the same height). */
|
|
49
49
|
const MAX_RENDER_HEIGHT = 20000;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
let browser = await chromium.launch();
|
|
51
|
+
let page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
|
|
52
52
|
const results = {};
|
|
53
53
|
const components = {};
|
|
54
54
|
const stateRows = {};
|
|
55
|
-
for (const s of screens) {
|
|
55
|
+
for (const [i, s] of screens.entries()) {
|
|
56
|
+
if (i > 0 && i % 50 === 0) {
|
|
57
|
+
// Chromium gets fragile after hundreds of page loads in one browser; a fresh one keeps long runs alive.
|
|
58
|
+
await page.close().catch(() => { });
|
|
59
|
+
await browser.close().catch(() => { });
|
|
60
|
+
browser = await chromium.launch();
|
|
61
|
+
page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
|
|
62
|
+
}
|
|
56
63
|
await page.setViewportSize({ width: s.width, height: 844 });
|
|
57
64
|
await page
|
|
58
65
|
.goto(`http://127.0.0.1:${server.port}/${s.path}`, { waitUntil: 'networkidle', timeout: 30_000 })
|
package/dist/report.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Consistency report: every shared component cropped from every active screen, next to the component
|
|
3
3
|
* catalog's version, with what differs. Writes <design>/consistency/index.html, which the canvas opens from
|
|
4
4
|
* its sidebar. Usage: stitch2 consistency [kind…] (kinds: tab-bar, header, icon-button, chip, …)
|
|
5
|
+
* Signatures and crops are cached in <design>/consistency/known.json: reruns only visit screens that are
|
|
6
|
+
* new, changed, or lack crops for the requested kinds, and check the issues against every screen.
|
|
5
7
|
*/
|
|
6
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
7
9
|
import { join } from 'node:path';
|
|
8
10
|
import { chromium } from '@playwright/test';
|
|
9
11
|
import { config } from './config.js';
|
|
@@ -21,12 +23,66 @@ const esc = (s) => String(s ?? '')
|
|
|
21
23
|
.replaceAll('<', '<')
|
|
22
24
|
.replaceAll('>', '>')
|
|
23
25
|
.replaceAll('"', '"');
|
|
26
|
+
const KNOWN_FILE = join(out, 'known.json');
|
|
27
|
+
const cached = existsSync(KNOWN_FILE)
|
|
28
|
+
? JSON.parse(readFileSync(KNOWN_FILE, 'utf8'))
|
|
29
|
+
: {};
|
|
30
|
+
// A screen needs a visit when it is new, changed, or lacks crops for the requested kinds.
|
|
31
|
+
const needsVisit = (path) => {
|
|
32
|
+
const hit = cached[path];
|
|
33
|
+
if (!hit)
|
|
34
|
+
return true;
|
|
35
|
+
try {
|
|
36
|
+
if (statSync(join(ROOT, path)).mtimeMs !== hit.mtime)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (!kinds.length)
|
|
43
|
+
return !hit.kinds.includes('all');
|
|
44
|
+
return !kinds.every((k) => hit.kinds.includes(k) || hit.kinds.includes('all'));
|
|
45
|
+
};
|
|
46
|
+
const todo = screens.filter((s) => needsVisit(s.path));
|
|
47
|
+
const todoPaths = new Set(todo.map((s) => s.path));
|
|
24
48
|
const server = await startServer(0);
|
|
25
|
-
const
|
|
26
|
-
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
|
|
49
|
+
const CONCURRENCY = 4;
|
|
27
50
|
const known = {};
|
|
28
51
|
const crops = {}; // kind -> path -> image files
|
|
52
|
+
// Reuse the cached signatures and crops of unchanged screens; only changed screens get a visit.
|
|
29
53
|
for (const s of screens) {
|
|
54
|
+
const hit = cached[s.path];
|
|
55
|
+
if (hit && !todoPaths.has(s.path)) {
|
|
56
|
+
known[s.path] = hit.components;
|
|
57
|
+
for (const [kind, files] of Object.entries(hit.crops)) {
|
|
58
|
+
if (kinds.length && !kinds.includes(kind.split(':')[0]))
|
|
59
|
+
continue;
|
|
60
|
+
crops[kind] ??= {};
|
|
61
|
+
crops[kind][s.path] = files;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const fresh = async () => {
|
|
66
|
+
const browser = await chromium.launch();
|
|
67
|
+
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
|
|
68
|
+
return { browser, page, visits: 0 };
|
|
69
|
+
};
|
|
70
|
+
const pool = await Promise.all(Array.from({ length: Math.min(CONCURRENCY, todo.length) }, fresh));
|
|
71
|
+
let nextWorker = 0;
|
|
72
|
+
/** Component families cropped per visited screen, merged into the cache. */
|
|
73
|
+
const visitedKinds = {};
|
|
74
|
+
const visit = async (s) => {
|
|
75
|
+
const worker = pool[nextWorker++ % pool.length];
|
|
76
|
+
const { page } = worker;
|
|
77
|
+
if (worker.visits > 0 && worker.visits % 50 === 0) {
|
|
78
|
+
// Chromium gets fragile after hundreds of page loads; a fresh browser keeps long runs alive.
|
|
79
|
+
await worker.page.close().catch(() => { });
|
|
80
|
+
await worker.browser.close().catch(() => { });
|
|
81
|
+
const next = await fresh();
|
|
82
|
+
pool[pool.indexOf(worker)] = next;
|
|
83
|
+
return visit(s);
|
|
84
|
+
}
|
|
85
|
+
worker.visits++;
|
|
30
86
|
await page.setViewportSize({ width: s.width, height: 844 });
|
|
31
87
|
await page
|
|
32
88
|
.goto(`http://127.0.0.1:${server.port}/${s.path}`, { waitUntil: 'networkidle', timeout: 30_000 })
|
|
@@ -34,9 +90,11 @@ for (const s of screens) {
|
|
|
34
90
|
await page.waitForTimeout(600);
|
|
35
91
|
const found = await page.evaluate(collectComponents, collectOptions());
|
|
36
92
|
known[s.path] = found.components;
|
|
93
|
+
visitedKinds[s.path] = new Set();
|
|
37
94
|
for (const [kind, sigs] of Object.entries(found.components)) {
|
|
38
95
|
if (kinds.length && !kinds.includes(kind.split(':')[0]))
|
|
39
96
|
continue;
|
|
97
|
+
visitedKinds[s.path].add(kind.split(':')[0]);
|
|
40
98
|
const files = [];
|
|
41
99
|
for (const [i, sig] of sigs.entries()) {
|
|
42
100
|
const file = `${safe(kind)}/${safe(s.path)}-${i}.png`;
|
|
@@ -73,8 +131,45 @@ for (const s of screens) {
|
|
|
73
131
|
crops[kind] ??= {};
|
|
74
132
|
crops[kind][s.path] = files;
|
|
75
133
|
}
|
|
134
|
+
visitedKinds[s.path].add('all');
|
|
135
|
+
};
|
|
136
|
+
await Promise.all(todo.map((s) => visit(s)));
|
|
137
|
+
// Remember what this run saw: visited screens with fresh mtimes, the rest untouched. Screens that are
|
|
138
|
+
// gone leave the cache; their rows leave the report with them.
|
|
139
|
+
const live = new Set(screens.map((s) => s.path));
|
|
140
|
+
const next = {};
|
|
141
|
+
for (const [path, hit] of Object.entries(cached)) {
|
|
142
|
+
if (live.has(path) && !todoPaths.has(path))
|
|
143
|
+
next[path] = hit;
|
|
144
|
+
}
|
|
145
|
+
for (const s of todo) {
|
|
146
|
+
const screenCrops = {};
|
|
147
|
+
for (const [kind, byPath] of Object.entries(crops)) {
|
|
148
|
+
if (byPath[s.path])
|
|
149
|
+
screenCrops[kind] = byPath[s.path];
|
|
150
|
+
}
|
|
151
|
+
const prev = cached[s.path];
|
|
152
|
+
const families = new Set([...(prev?.kinds ?? []), ...(visitedKinds[s.path] ?? [])]);
|
|
153
|
+
if (!kinds.length)
|
|
154
|
+
families.add('all');
|
|
155
|
+
else
|
|
156
|
+
for (const k of kinds)
|
|
157
|
+
families.add(k);
|
|
158
|
+
try {
|
|
159
|
+
next[s.path] = {
|
|
160
|
+
mtime: statSync(join(ROOT, s.path)).mtimeMs,
|
|
161
|
+
components: known[s.path],
|
|
162
|
+
kinds: [...families],
|
|
163
|
+
crops: { ...(prev?.crops ?? {}), ...screenCrops },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
76
167
|
}
|
|
77
|
-
|
|
168
|
+
writeFileSync(KNOWN_FILE, JSON.stringify(next));
|
|
169
|
+
await Promise.all(pool.map(async (w) => {
|
|
170
|
+
await w.page.close().catch(() => { });
|
|
171
|
+
await w.browser.close().catch(() => { });
|
|
172
|
+
}));
|
|
78
173
|
server.close();
|
|
79
174
|
const issues = consistencyIssues(known, Object.keys(known));
|
|
80
175
|
const ORDER = ['tab-bar', 'header', 'icon-button', 'button:primary:md', 'chip', 'section'];
|
package/package.json
CHANGED