@bridge-ds/visual 0.3.12 → 0.3.13
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/capture.d.ts +10 -0
- package/dist/diff.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +119 -21
- package/package.json +2 -2
package/dist/capture.d.ts
CHANGED
|
@@ -38,3 +38,13 @@ export declare function shoot(page: Page, r: ShotRequest): Promise<string | null
|
|
|
38
38
|
* click a story), much faster than a page load per story. Resolves once the story rendered or failed.
|
|
39
39
|
*/
|
|
40
40
|
export declare function switchStory(page: Page, storyId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* A page for `bridge specs --measure`: an installed Chrome, one page at 1200x800, light scheme, reduced motion. Unlike
|
|
43
|
+
* the capture context it reaches remote hosts (a team's deployed Storybook). Close it when done.
|
|
44
|
+
*/
|
|
45
|
+
export declare function openMeasurePage(o?: LaunchOptions & {
|
|
46
|
+
scale?: number;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
page: Page;
|
|
49
|
+
close: () => Promise<void>;
|
|
50
|
+
}>;
|
package/dist/diff.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface DiffResult {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const decodePng: (buf: Buffer) => PNG;
|
|
14
14
|
export declare const encodePng: (png: PNG) => Buffer;
|
|
15
|
+
/** The `x, y, width, height` region of `src` (px of the image), clipped to it. */
|
|
16
|
+
export declare function cropPng(src: PNG, x: number, y: number, width: number, height: number): PNG;
|
|
15
17
|
/** Copies `src` onto a `width` x `height` canvas filled with an opaque neutral, so sizes can differ. */
|
|
16
18
|
export declare function padTo(src: PNG, width: number, height: number): PNG;
|
|
17
19
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { runImpact, planImpact, changedTokens, type ImpactOptions, type ImpactResult, type Plan } from './run.js';
|
|
2
2
|
export { selectImpact, storiesFromSource, storyId, storyComponent, type SelectInput } from './select.js';
|
|
3
|
-
export { diffPngs, boxesFromDiff, boxesFromCells, padTo, decodePng, encodePng, type DiffResult } from './diff.js';
|
|
3
|
+
export { diffPngs, boxesFromDiff, boxesFromCells, padTo, cropPng, decodePng, encodePng, type DiffResult } from './diff.js';
|
|
4
4
|
export { COMMENT_MARKER, hasVisualChange, rankShots, renderComment, summarize, summaryLine, type CommentOptions } from './manifest.js';
|
|
5
5
|
export { DEFAULT_ROUTES, DEFAULT_TENANTS, routeUrl, storyUrl, shotKey } from './routes.js';
|
|
6
6
|
export { captureFigmaReferences, type ReferencesOptions, type ReferencesResult } from './references.js';
|
|
7
7
|
export { linkNodeModules } from './workspace.js';
|
|
8
|
+
export { openMeasurePage } from './capture.js';
|
|
8
9
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -121,6 +121,12 @@ var CONFIG_FILE = "bridge.config.json";
|
|
|
121
121
|
var CHANGES_MODES = ["suggest", "direct"];
|
|
122
122
|
var DEFAULT_CHANGES_MODE = "suggest";
|
|
123
123
|
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
124
|
+
var TEAM_NAME_MAX = 60;
|
|
125
|
+
function teamName(value) {
|
|
126
|
+
if (typeof value !== "string") return void 0;
|
|
127
|
+
const v = value.trim().replace(/\s+/g, " ");
|
|
128
|
+
return v && v.length <= TEAM_NAME_MAX ? v : void 0;
|
|
129
|
+
}
|
|
124
130
|
var DEFAULT_TOKEN_PATHS = {
|
|
125
131
|
root: "tokens",
|
|
126
132
|
primitives: "primitives",
|
|
@@ -226,6 +232,7 @@ function defineConfig(input) {
|
|
|
226
232
|
const g = input.governance ?? {};
|
|
227
233
|
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
228
234
|
return {
|
|
235
|
+
...teamName(input.name) ? { name: teamName(input.name) } : {},
|
|
229
236
|
repo: parseRepoInput(input.repo, input.branch),
|
|
230
237
|
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
|
|
231
238
|
axes,
|
|
@@ -264,6 +271,7 @@ function defineConfig(input) {
|
|
|
264
271
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
265
272
|
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
266
273
|
...input.library?.public === true ? { library: { public: true } } : {},
|
|
274
|
+
...input.demo?.viewer?.trim() ? { demo: { viewer: input.demo.viewer.trim(), ...input.demo.role?.trim() ? { role: input.demo.role.trim() } : {} } } : {},
|
|
267
275
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
268
276
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
269
277
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -388,6 +396,7 @@ function loadConfig(source) {
|
|
|
388
396
|
}
|
|
389
397
|
}
|
|
390
398
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
399
|
+
if (d.name !== void 0 && teamName(d.name) === void 0) problems.push(`"name" must be the team's name, 1 to ${TEAM_NAME_MAX} characters`);
|
|
391
400
|
if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
|
|
392
401
|
const sb = d.storybook;
|
|
393
402
|
if (sb !== void 0) {
|
|
@@ -403,6 +412,14 @@ function loadConfig(source) {
|
|
|
403
412
|
for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
|
|
404
413
|
}
|
|
405
414
|
}
|
|
415
|
+
const demo = d.demo;
|
|
416
|
+
if (demo !== void 0) {
|
|
417
|
+
if (!isObj(demo) || !isStr(demo.viewer) || !demo.viewer.trim()) problems.push('"demo" must be { "viewer" }, for example { "viewer": "Dana", "role": "DS lead" }');
|
|
418
|
+
else {
|
|
419
|
+
if (demo.role !== void 0 && !isStr(demo.role)) problems.push('"demo.role" must be a string, for example "DS lead"');
|
|
420
|
+
for (const k of Object.keys(demo)) if (k !== "viewer" && k !== "role") problems.push(`"demo.${k}" is not a known key`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
406
423
|
problems.push(...productProblems(d.products));
|
|
407
424
|
const audit = d.audit;
|
|
408
425
|
if (audit !== void 0) {
|
|
@@ -897,7 +914,7 @@ var STATE_PSEUDO = {
|
|
|
897
914
|
var iconName = (component) => component.replace(/^Icon\//, "");
|
|
898
915
|
var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
|
|
899
916
|
function planReference(spec, variant, file) {
|
|
900
|
-
if (spec.view === "popup") return
|
|
917
|
+
if (spec.view === "popup") return planPopup(spec, variant);
|
|
901
918
|
const props = { ...spec.fixed ?? {} };
|
|
902
919
|
let pseudo;
|
|
903
920
|
for (const axis of spec.axes) {
|
|
@@ -926,6 +943,7 @@ function planReference(spec, variant, file) {
|
|
|
926
943
|
if (part.swap && swapTo) {
|
|
927
944
|
const slot = file?.slots?.find((s) => s.name === swapTo);
|
|
928
945
|
if (!slot) return { reason: `${swapTo} is not in the specs` };
|
|
946
|
+
if (slot.kind === "media") return { reason: `Figma shows a media placeholder (${swapTo}); code shows the image it is given, nothing to compare` };
|
|
929
947
|
if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
|
|
930
948
|
return slot.text.default;
|
|
931
949
|
}
|
|
@@ -977,10 +995,13 @@ function planReference(spec, variant, file) {
|
|
|
977
995
|
}
|
|
978
996
|
const example = spec.example;
|
|
979
997
|
const exampleChildren = example?.children?.length ? example.children : void 0;
|
|
998
|
+
const component = exportName(example?.c ?? (spec.name.includes("/") ? spec.component ?? spec.name : spec.name));
|
|
999
|
+
const parent = spec.component ? exportName(spec.component) : void 0;
|
|
980
1000
|
return {
|
|
981
1001
|
ok: true,
|
|
982
1002
|
render: {
|
|
983
|
-
component
|
|
1003
|
+
component,
|
|
1004
|
+
...parent && parent !== component ? { within: parent } : {},
|
|
984
1005
|
props: { ...example?.p ?? {}, ...props },
|
|
985
1006
|
...exampleChildren ? { children: exampleChildren } : {},
|
|
986
1007
|
...text !== void 0 && !exampleChildren ? { text } : {},
|
|
@@ -991,6 +1012,28 @@ function planReference(spec, variant, file) {
|
|
|
991
1012
|
}
|
|
992
1013
|
};
|
|
993
1014
|
}
|
|
1015
|
+
function planPopup(spec, variant) {
|
|
1016
|
+
const root = spec.parts.find((p) => !p.parent);
|
|
1017
|
+
const own = {};
|
|
1018
|
+
for (const axis of spec.axes) {
|
|
1019
|
+
const value = variant.props[axis.name];
|
|
1020
|
+
if (!axis.prop || value === void 0) return { ok: false, reason: `${axis.name} has no matching option in code` };
|
|
1021
|
+
own[axis.prop] = value === "true" ? true : value === "false" ? false : value;
|
|
1022
|
+
}
|
|
1023
|
+
const className = /\.module\.css \.([\w-]+)/.exec(root?.source ?? "")?.[1];
|
|
1024
|
+
const component = spec.component ? exportName(spec.component) : void 0;
|
|
1025
|
+
const example = spec.example;
|
|
1026
|
+
if (!component || !example) return { ok: false, reason: "drawn open from no code example: nothing to open in code" };
|
|
1027
|
+
if (!className) return { ok: false, reason: "the popup has no class to find it by in code" };
|
|
1028
|
+
const popup = { className };
|
|
1029
|
+
if (exportName(example.c) === component) {
|
|
1030
|
+
return { ok: true, render: { component, props: { ...example.p ?? {}, ...own, defaultOpen: true }, ...example.children?.length ? { children: example.children } : {}, popup } };
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
ok: true,
|
|
1034
|
+
render: { component, props: { defaultOpen: true }, children: [{ c: `${component}Trigger`, children: ["Open"] }, { ...example, p: { ...example.p ?? {}, ...own } }], popup }
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
994
1037
|
function referenceImagePath(component, key) {
|
|
995
1038
|
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
996
1039
|
const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
|
|
@@ -1214,6 +1257,17 @@ async function switchStory(page, storyId2) {
|
|
|
1214
1257
|
);
|
|
1215
1258
|
if (!ok) throw new Error("Storybook channel not found");
|
|
1216
1259
|
}
|
|
1260
|
+
async function openMeasurePage(o = {}) {
|
|
1261
|
+
const browser = await launch(o);
|
|
1262
|
+
try {
|
|
1263
|
+
const ctx = await browser.newContext({ viewport: { width: 1200, height: 800 }, deviceScaleFactor: o.scale ?? 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", serviceWorkers: "block" });
|
|
1264
|
+
const page = await ctx.newPage();
|
|
1265
|
+
return { page, close: () => browser.close().catch(() => void 0) };
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
await browser.close().catch(() => void 0);
|
|
1268
|
+
throw e;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1217
1271
|
|
|
1218
1272
|
// src/composite.ts
|
|
1219
1273
|
import { readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -1265,6 +1319,15 @@ import pixelmatch from "pixelmatch";
|
|
|
1265
1319
|
import { PNG } from "pngjs";
|
|
1266
1320
|
var decodePng = (buf) => PNG.sync.read(buf);
|
|
1267
1321
|
var encodePng = (png) => PNG.sync.write(png, { colorType: 6 });
|
|
1322
|
+
function cropPng(src, x, y, width, height) {
|
|
1323
|
+
const left = Math.max(0, Math.round(x));
|
|
1324
|
+
const top = Math.max(0, Math.round(y));
|
|
1325
|
+
const w = Math.max(1, Math.min(src.width - left, Math.round(width)));
|
|
1326
|
+
const h = Math.max(1, Math.min(src.height - top, Math.round(height)));
|
|
1327
|
+
const out = new PNG({ width: w, height: h });
|
|
1328
|
+
PNG.bitblt(src, out, left, top, w, h, 0, 0);
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1268
1331
|
function padTo(src, width, height) {
|
|
1269
1332
|
if (src.width === width && src.height === height) return src;
|
|
1270
1333
|
const out = new PNG({ width, height });
|
|
@@ -2105,9 +2168,12 @@ async function captureFigmaReferences(o) {
|
|
|
2105
2168
|
try {
|
|
2106
2169
|
const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
|
|
2107
2170
|
const page = await newPage(ctx);
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2171
|
+
const openStory = async () => {
|
|
2172
|
+
await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
|
|
2173
|
+
await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
|
|
2174
|
+
await settle(page, { busy: false });
|
|
2175
|
+
};
|
|
2176
|
+
await openStory();
|
|
2111
2177
|
log(`Capturing ${jobs.length} variants at ${scale}x (${brand} ${theme})`);
|
|
2112
2178
|
for (const j of jobs) {
|
|
2113
2179
|
const variants = manifest.components[j.component].variants;
|
|
@@ -2116,25 +2182,17 @@ async function captureFigmaReferences(o) {
|
|
|
2116
2182
|
skipped++;
|
|
2117
2183
|
continue;
|
|
2118
2184
|
}
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
resolve2();
|
|
2127
|
-
};
|
|
2128
|
-
const timer = setTimeout(done, 1e4);
|
|
2129
|
-
ch.on("storyRendered", done);
|
|
2130
|
-
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
2131
|
-
}),
|
|
2132
|
-
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
2133
|
-
);
|
|
2185
|
+
const shown = await renderPlan(page, JSON.stringify(j.plan.render));
|
|
2186
|
+
if (shown.broken) {
|
|
2187
|
+
variants[j.key] = { skipped: `the code example threw: ${shown.error ?? "the story did not finish rendering"}` };
|
|
2188
|
+
skipped++;
|
|
2189
|
+
await openStory();
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2134
2192
|
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
2135
2193
|
const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
|
|
2136
2194
|
let target = page.locator("[data-fidelity-root] > *").first();
|
|
2137
|
-
if (!error && !await target.count()) {
|
|
2195
|
+
if (!error && (j.plan.render.within || j.plan.render.popup || !await target.count())) {
|
|
2138
2196
|
const marked = page.locator("[data-fidelity-target]").first();
|
|
2139
2197
|
await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
2140
2198
|
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
@@ -2145,6 +2203,15 @@ async function captureFigmaReferences(o) {
|
|
|
2145
2203
|
skipped++;
|
|
2146
2204
|
continue;
|
|
2147
2205
|
}
|
|
2206
|
+
const transparent = await target.evaluate((el) => {
|
|
2207
|
+
for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).opacity === "0") return true;
|
|
2208
|
+
return false;
|
|
2209
|
+
});
|
|
2210
|
+
if (transparent) {
|
|
2211
|
+
variants[j.key] = { skipped: "the code example rendered invisible (opacity 0)" };
|
|
2212
|
+
skipped++;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2148
2215
|
const file = referenceImagePath(j.component, j.key);
|
|
2149
2216
|
const path = join4(o.out, file);
|
|
2150
2217
|
await mkdir4(dirname3(path), { recursive: true });
|
|
@@ -2165,6 +2232,35 @@ async function captureFigmaReferences(o) {
|
|
|
2165
2232
|
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
2166
2233
|
return { manifest, manifestPath, captured, skipped };
|
|
2167
2234
|
}
|
|
2235
|
+
async function renderPlan(page, plan) {
|
|
2236
|
+
const r = await page.evaluate(
|
|
2237
|
+
({ id, plan: plan2 }) => new Promise((resolve2) => {
|
|
2238
|
+
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
2239
|
+
let error;
|
|
2240
|
+
const failed = (p) => {
|
|
2241
|
+
const e = p;
|
|
2242
|
+
error ??= e?.message ?? e?.title ?? e?.description;
|
|
2243
|
+
};
|
|
2244
|
+
const finish = (rendered) => {
|
|
2245
|
+
clearTimeout(timer);
|
|
2246
|
+
ch.off("storyRendered", done);
|
|
2247
|
+
ch.off("storyThrewException", failed);
|
|
2248
|
+
ch.off("storyErrored", failed);
|
|
2249
|
+
resolve2({ rendered, ...error ? { error } : {} });
|
|
2250
|
+
};
|
|
2251
|
+
const done = () => finish(true);
|
|
2252
|
+
document.querySelectorAll("[data-fidelity-target]").forEach((el) => el.removeAttribute("data-fidelity-target"));
|
|
2253
|
+
const timer = setTimeout(() => finish(false), 1e4);
|
|
2254
|
+
ch.on("storyThrewException", failed);
|
|
2255
|
+
ch.on("storyErrored", failed);
|
|
2256
|
+
ch.on("storyRendered", done);
|
|
2257
|
+
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
2258
|
+
}),
|
|
2259
|
+
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
2260
|
+
);
|
|
2261
|
+
const errorScreen = await page.evaluate(() => document.body.classList.contains("sb-show-errordisplay"));
|
|
2262
|
+
return { broken: !r.rendered || !!r.error || errorScreen, ...r.error ? { error: r.error } : {} };
|
|
2263
|
+
}
|
|
2168
2264
|
export {
|
|
2169
2265
|
COMMENT_MARKER,
|
|
2170
2266
|
DEFAULT_ROUTES,
|
|
@@ -2173,11 +2269,13 @@ export {
|
|
|
2173
2269
|
boxesFromDiff,
|
|
2174
2270
|
captureFigmaReferences,
|
|
2175
2271
|
changedTokens,
|
|
2272
|
+
cropPng,
|
|
2176
2273
|
decodePng,
|
|
2177
2274
|
diffPngs,
|
|
2178
2275
|
encodePng,
|
|
2179
2276
|
hasVisualChange,
|
|
2180
2277
|
linkNodeModules,
|
|
2278
|
+
openMeasurePage,
|
|
2181
2279
|
padTo,
|
|
2182
2280
|
planImpact,
|
|
2183
2281
|
rankShots,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/visual",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge visual impact analysis: screenshots the stories and app screens a design system change touches, at the base and head refs, and pixel-diffs them into before/after/diff images and a JSON manifest.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"build:package": "node ../../scripts/build-packages.mjs visual"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@bridge-ds/core": "^0.
|
|
50
|
+
"@bridge-ds/core": "^0.19.0",
|
|
51
51
|
"pixelmatch": "7.1.0",
|
|
52
52
|
"playwright-core": "1.63.0",
|
|
53
53
|
"pngjs": "7.0.0"
|